首页 > 编程语言 >理解Java中的ThreadLocal

理解Java中的ThreadLocal

时间:2023-05-29 12:32:42浏览次数:45  
标签:Java Thread ThreadLocalMap ThreadLocal 理解 线程 内存 new


提到ThreadLocal,有些Android或者Java程序员可能有所陌生,可能会提出种种问题,它是做什么的,是不是和线程有关,怎么使用呢?等等问题,本文将总结一下我对ThreadLocal的理解和认识,希望让大家理解ThreadLocal更加透彻一些。

ThreadLocal是什么

理解Java中的ThreadLocal_内存泄露


 

ThreadLocal是一个关于创建线程局部变量的类。

通常情况下,我们创建的变量是可以被任何一个线程访问并修改的。而使用ThreadLocal创建的变量只能被当前线程访问,其他线程则无法访问和修改。

Global && Local

上面的两个修饰看似矛盾,实则不然。

  • Global 意思是在当前线程中,任何一个点都可以访问到ThreadLocal的值。
  • Local 意思是该线程的ThreadLocal只能被该线程访问,一般情况下其他线程访问不到。

用法简介

创建,支持泛型


1
ThreadLocal<String> mStringThreadLocal = new ThreadLocal<>();


set方法


1
mStringThreadLocal.set("droidyue.com");


get方法


1
mStringThreadLocal.get();


完整的使用示例


1
2
3
4
5
6
7
8
9
10
11
12
13
14
private void testThreadLocal() {
    Thread t = new Thread() {
        ThreadLocal<String> mStringThreadLocal = new ThreadLocal<>();
        @Override
        public void run() {
            super.run();
            mStringThreadLocal.set("droidyue.com");
            mStringThreadLocal.get();
        }
    };
    t.start();
}


ThreadLocal初始值

为ThreadLocal设置默认的get初始值,需要重写initialValue方法,下面是一段代码,我们将默认值修改成了线程的名字


1
2
3
4
5
6
ThreadLocal<String> mThreadLocal = new ThreadLocal<String>() {
    @Override
    protected String initialValue() {
      return Thread.currentThread().getName();
    }
};


Android中的应用

在Android中,Looper类就是利用了ThreadLocal的特性,保证每个线程只存在一个Looper对象。


1
2
3
4
5
6
7
static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();
private static void prepare(boolean quitAllowed) {
    if (sThreadLocal.get() != null) {
        throw new RuntimeException("Only one Looper may be created per thread");
    }
    sThreadLocal.set(new Looper(quitAllowed));
}


如何实现

为了更好的掌握ThreadLocal,我认为了解其内部实现是很有必要的,这里我们以set方法从起始看一看ThreadLocal的实现原理。

下面是ThreadLocal的set方法,大致意思为

  • 首先获取当前线程
  • 利用当前线程作为句柄获取一个ThreadLocalMap的对象
  • 如果上述ThreadLocalMap对象不为空,则设置值,否则创建这个ThreadLocalMap对象并设置值

源码如下


1
2
3
4
5
6
7
8
public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}


下面是一个利用Thread对象作为句柄获取ThreadLocalMap对象的代码


1
2
3
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}


上面的代码获取的实际上是Thread对象的threadLocals变量,可参考下面代码


1
2
3
4
5
6
class Thread implements Runnable {
    /* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;
}


而如果一开始设置,即ThreadLocalMap对象未创建,则新建ThreadLocalMap对象,并设置初始值。


1
2
3
void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}


总结:实际上ThreadLocal的值是放入了当前线程的一个ThreadLocalMap实例中,所以只能在本线程中访问,其他线程无法访问。

对象存放在哪里

在Java中,栈内存归属于单个线程,每个线程都会有一个栈内存,其存储的变量只能在其所属线程中可见,即栈内存可以理解成线程的私有内存。而堆内存中的对象对所有线程可见。堆内存中的对象可以被所有线程访问。

问:那么是不是说ThreadLocal的实例以及其值存放在栈上呢?

其实不是,因为ThreadLocal实例实际上也是被其创建的类持有(更顶端应该是被线程持有)。而ThreadLocal的值其实也是被线程实例持有。

它们都是位于堆上,只是通过一些技巧将可见性修改成了线程可见。

关于堆和栈的比较,请参考Java中的堆和栈的区别

真的只能被一个线程访问么

既然上面提到了ThreadLocal只对当前线程可见,是不是说ThreadLocal的值只能被一个线程访问呢?

使用InheritableThreadLocal可以实现多个线程访问ThreadLocal的值。

如下,我们在主线程中创建一个InheritableThreadLocal的实例,然后在子线程中得到这个InheritableThreadLocal实例设置的值。


1
2
3
4
5
6
7
8
9
10
11
12
13
private void testInheritableThreadLocal() {
    final ThreadLocal threadLocal = new InheritableThreadLocal();
    threadLocal.set("droidyue.com");
    Thread t = new Thread() {
        @Override
        public void run() {
            super.run();
            Log.i(LOGTAG, "testInheritableThreadLocal =" + threadLocal.get());
        }
    };
    t.start();
}


上面的代码输出的日志信息为


1
I/MainActivity( 5046): testInheritableThreadLocal =droidyue.com


使用InheritableThreadLocal可以将某个线程的ThreadLocal值在其子线程创建时传递过去。因为在线程创建过程中,有相关的处理逻辑。


1
2
3
4
5
6
7
8
9
10
11
12
13
//Thread.java
 private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc) {
        //code goes here
        if (parent.inheritableThreadLocals != null)
            this.inheritableThreadLocals =
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
        /* Stash the specified stack size in case the VM cares */
        this.stackSize = stackSize;
        /* Set thread ID */
        tid = nextThreadID();
}


上面代码就是在线程创建的时候,复制父线程的inheritableThreadLocals的数据。

会导致内存泄露么

有网上讨论说ThreadLocal会导致内存泄露,原因如下

  • 首先ThreadLocal实例被线程的ThreadLocalMap实例持有,也可以看成被线程持有。
  • 如果应用使用了线程池,那么之前的线程实例处理完之后出于复用的目的依然存活
  • 所以,ThreadLocal设定的值被持有,导致内存泄露。

上面的逻辑是清晰的,可是ThreadLocal并不会产生内存泄露,因为ThreadLocalMap在选择key的时候,并不是直接选择ThreadLocal实例,而是ThreadLocal实例的弱引用。


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static class ThreadLocalMap {
/**
* The entries in this hash map extend WeakReference, using
* its main ref field as the key (which is always a
* ThreadLocal object).  Note that null keys (i.e. entry.get()
* == null) mean that the key is no longer referenced, so the
* entry can be expunged from table.  Such entries are referred to
* as "stale entries" in the code that follows.
*/
    static class Entry extends WeakReference<ThreadLocal<?>> {
        /** The value associated with this ThreadLocal. */
        Object value;
        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
    }
}


所以实际上从ThreadLocal设计角度来说是不会导致内存泄露的。关于弱引用,了解更多,请访问译文:理解Java中的弱引用

使用场景

  • 实现单个线程单例以及单个线程上下文信息存储,比如交易id等
  • 实现线程安全,非线程安全的对象使用ThreadLocal之后就会变得线程安全,因为每个线程都会有一个对应的实例
  • 承载一些线程相关的数据,避免在方法中来回传递参数

注意:Android的ThreadLocal与Java实现略有不同,但是原理是一致的

标签:Java,Thread,ThreadLocalMap,ThreadLocal,理解,线程,内存,new
From: https://blog.51cto.com/u_16131764/6370086

相关文章

  • java虚拟机总结
     类型的生命周期:java虚拟机通过装载、连接和初始化一个java类型,使该类型可以被正在运行的java程序所使用。装载:是把二进制形式的java类型读入java虚拟机中。连接:是把读入的二进制形式的类型数据合并到虚拟机的运行时状态中去。连接分三个子步骤(验证、准      备和解析......
  • mysql、sqlserver、oracle分页,java分页统一接口实现
    定义:pageStart起始页,pageEnd终止页,pageSize页面容量oracle分页:rownum numfrom(实际传的SQL)where rownum<=pageEnd)wherenum>=pageStartsqlServer分页:           select*from(select top 页面容量from(select top字段Adesc)astemptable2orderb......
  • C++头文件理解
    看了下公司内部对于头文件的使用,感觉挺乱的。对于头文件容易搞混淆的点在于:涉及多文件,多次包含的时候容易搞错比如上面CPP就有三四个文件,每一个CPP都要按照先包含config,后包含common的顺序,在编译的时候指明。但是include头文件本质上就是:在编译时把指定的文件,包含到调用的地方......
  • java爬虫htmlunit模拟浏览器登录
    介绍刚学到了一种超实用的java爬虫技术htmlunit,先记录一下。htmlunit其实就是一个没有界面的浏览器,操作很简单,就像自己在使用浏览器。本文介绍其简单的几个操作,仅初学了解htmlunit。第一是模拟登录网站,第二是获取网页html源码。准备下载htmlunit的jar包,点击进入官网下载,下载后,里面......
  • java内存溢出监控
    在程序内增加内存溢出字符串 使用命令启动jar  -XX:+HeapDumpOnOutOfMemoryError这个命令会在报内存溢出的时候生成 .hprof文件(1)java-XX:+HeapDumpOnOutOfMemoryError-jarzxhs.jar(2)java-Xms128m-Xmx256m-XX:+HeapDumpOnOutOfMemoryError-jarzxhs.jar下......
  • java.lang.IllegalArgumentException: Invalid character found in method name [toke
    这个问题是本地用了https,只要将https改为http就可以解决。  参考:https://blog.csdn.net/weixin_44299027/article/details/109474606https://blog.csdn.net/jcmj123456/article/details/124002200......
  • 对SpringIOC和SpringAOP的理解
    SpringIOC和SpringAOP是Spring的两个核心组件。SpringIOC:SpringIOC是一个管理bean的容器,能够帮我们管理bean的整个生命周期,在没有SpringIOC的时候,我们需要自己手动的管理bean以及bean的依赖关系,这样会增加耦合,而有了SpringIOC,它能帮我们管理bean以及bean的依赖关系,使得代码解耦。......
  • 开发 Java笔记
    1.Controller@RequestMapping注解用于绑定URI到具体处理器。@RestController:Spring4新增注解,同样可以注解Controller类,相当于@Controller+@ResponseBody,主要是为了使http请求返回 json 或者xml格式数据,一般情况下都是使用这个注解。下文都基于此注解进行验证。用于将......
  • java8 stream匹配 anyMatch,allMatch,noneMatch
    anyMatch:判断的条件里,任意一个元素成功,返回trueallMatch:判断条件里的元素,所有的都是,返回truenoneMatch:与allMatch相反,判断条件里的元素,所有的都不是,返回truecount方法,跟List接口中的.size()一样,返回的都是这个集合流的元素的长度,不同的是,流是集合的一个高级工厂,中间操作是工厂里......
  • Java并发编程之ConcurrentLinkedQueue详解
    简介在并发编程中我们有时候需要使用线程安全的队列。如果我们要实现一个线程安全的队列有两种实现方式一种是使用阻塞算法,另一种是使用非阻塞算法。使用阻塞算法的队列可以用一个锁(入队和出队用同一把锁)或两个锁(入队和出队用不同的锁)等方式来实现,而非阻塞的实现方式则可以使用循......