目录

1. ThreadLocal 的主要功能?

2. ThreadLocal 代码举例

3. ThreadLocal 源码分析

3.1 ThreadLocal 的 get 方法源码解析

3.2 ThreadLocal 的 set 方法源码解析

3.3 ThreadLocal 的 createMap 方法源码解析

3.4 ThreadLocal 的 set 方法总结

4. 为什么Entry要使用弱引用指向ThreadLocal

5. 如果将 tl 设置为 null,一定要将Map中的对应记录remove

6. 线程池中的线程使用完毕归还之前清除Map

7. ThreadLocal 广泛应用于线程池


1. ThreadLocal 的主要功能?

关于 ThreadLocal 的核心功能我大致总结了以下三点

(1)线程并发:ThreadLocal 更多应用于多线程并发的场景下;

(2)传递数据:我们可以通过 ThreadLocal 在同一个线程,不同组件中传递公共变量;

(3)线程隔离:每个线程的变量都是独立隔离的,不会互相影响;

或许有些同学听完之后会有些疑惑,我来举一个简单的场景,如下图所示,假定现在有一个线程,里面有一个变量X,最先执行a方法,a方法中调用了b方法,b方法中调用了c方法,c方法又调用了d方法,层层调用,我现在问,随着调用层次的加深,层次深得方法还能获取这个变量X吗?

答案是不一定。

有些同学可能会说,我把这个参数每一层方法都传递一次不就可以了吗?这还真不一定行,如果中间你的方法调用了某些第三方库,或者做了一些别的业务逻辑,变量X就不一定能继续传递下去了;

还有的同学会说,这个简单,我把变量X设置为 static 静态的不就行了吗,所有人共享,一下子就获取到了。哎,这个也不行哦,因为我现在只是举了一个线程,如果这个变量X是一个共享资源,会有多个线程会操作它,此时它就是线程不安全的,所以设置为 static 也是不行的。

既然如此,那该怎么做呢?

这里就要用到本篇文章要说的 ThreadLocal,通过使用 ThreadLocal,我们就可以使变量X在多线程并发的情况下也能线程安全的使用变量X并且不需要上锁,而且还做到各个线程之间相互隔离,达到在同一个线程中任何时刻都能够获取到变量X的效果,是不是非常厉害,那么下面我们就一起来探究 ThreadLocal 使用和底层逻辑吧。

2. ThreadLocal 代码举例

如下代码,我都做了一些注释,应该不难理解                                                                

public class WeakReferenceTest {
    public static void main(String[] args) {
        // 定义一个 ThreadLocal 全局变量 tl
        ThreadLocal tl = new ThreadLocal();
        // 创建一个线程
        new Thread(() -> {
            // 向 tl 对象中存放一个 WeakReferenceTest 的类对象
            tl.set(new WeakReferenceTest());
            // 存放对象之后获取,看能否获取成功
            System.out.println(tl.get()+"存放当前类对象的线程获取存放的类对象");
        }).start();

        // 再创建一个新的线程
        new Thread(() -> {
            // 让该线程睡1秒,保证上一个线程比这个线程先执行
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // get 取出上一个线程存放的类对象,看啊可能能否获取到
            System.out.println(tl.get()+"非存放当前类对象的线程获取存放的类对象");
        }).start();
    }
}

然后我们运行上述方法,可以在控制台中得到如下图所示的结果,第一个线程自己向 ThreadLocal 对象中存放创建的类对象时,能够获取得到;但是第二个线程从 ThreadLocal 对象中获取第一个线程存放的类对象时,获取不到,得到的是null。但是我们明明把 ThreadLocal 对象 tl 定义成全局变量了啊,怎么会拿不到呢?

这就是 ThreadLocal 对象特有的属性,从这个例子中也不难看出,ThreadLocal 对象对于线程有自带的隔离性,就算是多个线程共用一个 ThreadLocal 对象,但它们之间存取数据互相不可见,只有自己存储的数据自己能得到,不会对其他线程产生影响。

3. ThreadLocal 源码分析

通过刚才的代码举例演示,同学们也应该简单了解了 ThreadLocal 的特性,那么它是如何做到多线程隔离性的呢?这就要通过阅读它的源码了解底层原理。

3.1 ThreadLocal 的 get 方法源码解析

我们看一下 ThreadLocal 的 set 方法的源码如下所示,方法中它获取了当前线程对象,并用 t 来接收,接收之后调用了 getMap 方法,我们点击跟进查看一下 getMap 方法的源码

如下所示,该方法在 ThreadLocal 类中,找到了 getMap 方法,该方法源码注释最后一行也说了,方法返回值是一个 Map 集合。

方法 return t.threadLocals ,我们点击查看一下它到底是什么东西,如下所示

此时可以看到,我们跳转到了 Thread 类中,源码中 threadLocals 默认为null,它的类型是 ThreadLocalMap,我们点击它查看一下,

此时又跳转到了 ThreadLocal 类中,ThreadLocalMap 是 ThreadLocal 类中的一个内部类,它维护了一个 Map 数组,INITIAL_CAPACITY 初始容量为16, 

说明了在 Java 底层,每个Thread 线程对象底层都维护了一个 Map 数组

3.2 ThreadLocal 的 set 方法源码解析

接着刚才 3.1 的继续说,获取到当前线程对象的 map 之后,然后对 map 做判断,如果 map 集合不为空,说明已经存在,则将(this,value)存入其中,认真分析,此时的 this 是什么,是方法的调用者即 tl 对象,也就是说 ThreadLocal 的对象 tl 是 Map 集合的 key,value值就是我们方法要传入的 value 值;

点击跟如查看 set 方法的源码,这里它有一个 Entry,Entry 最终存放到了 Map 集合中去,其实一个 Entry 对象就是 Map 集合中的一条记录,Entry 对象内部就是KV键值对,K就是 ThreadLocal 对象 tl ,V 就是方法想要保存的数据 value 值。

下面我们点击 Entry 查看,发现 Entry 是 ThreadLocalMap 类中的内部类,并且该类继承了 WeakReference<>弱引用类。到现在我们发现,ThreadLocal 竟然与弱引用有所关联,并且调用了父类 super 的方法,所以也就是说它创建的对象引用类型是弱引用类型。

弱引用对象最重要的一点就是只要遇到GC垃圾回收,这个弱引用对象就会被回收清理;

如果有同学想深入了解弱引用原理的,可以去看我的另一篇文章,对强引用,弱引用,软引用,虚引用均有所介绍。建议先搞明白什么是弱引用再来学习 ThreadLocal,有助于更深入的理解 ThreadLocal 的原理。

强引用,弱引用,软引用,虚引用它们有什么区别?你知道吗?-CSDN博客icon-default.png?t=N7T8https://blog.csdn.net/m0_70325779/article/details/133268853?spm=1001.2014.3001.5501

3.3 ThreadLocal 的 createMap 方法源码解析

接着刚才的 3.2 ,如果 map 集合为空,说明当前线程应该是首次设置 ThreadLocal 属性,所以需要调用 createMap 方法先将当前线程的 map 集合创建出来,然后再将 value 值设置其中。流程与 if 语句中差不多,都是创建的引用类型对象然后存放其中。

createMap 源码如下所示,这里也可以看到它确实创建了一个 ThreadLocalMap 集合

点击 ThreadLocal 的带参构造,即可跳转至如下界面,这里底层 new 了一个大小为 16 的 Map 集合 

3.4 ThreadLocal 的 set 方法总结

经过上面三点的说明,我画了一个的简化图,

(1)程序中我们 new 一个 ThreadLocal 对象用 tl 接收,也可以 new 多个,每个 tl 都用来存储一个多线程都想要使用的变量并且不希望出现线程不安全的情况;

(2)每个线程内部都有一个 tls 属性指向独属于自己的那个 map 集合,集合内部存放的就是 Entry 对象;

(3)当我们调用 tl 对象 set 方法的时候,实际上就是向每个线程独有的 map 集合中存放一个个 Entry 键值对对象,Key 是 set 方法的调用者 tl,Value 则是我们要保存的 value 值;

(4)而且在进行 set 存放 Entry 操作的时候底层创建的 Entry 对象则是弱引用类型,存放在 Map 集合中;

4. 为什么Entry要使用弱引用指向ThreadLocal

经过上面的分析,我们知道了,Map 集合中 Entry 的 key 采用弱引用的方式指向 ThreadLcoal 对象,但是为什么要采用弱引用呢?

因为弱引用可以巧妙地解决 ThreadLocal 带来的内存泄漏问题。

举例:假设现在我的程序中使用 ThreadLocal tl = new ThreadLocal() 定义了一个 ThreadLocal 全局变量,并且在当线程执行了 tl.set 将 tl 全局变量备份了一份存储到了当前线程私有的 map 集合中,随着程序的运行,我后面可能不再需要 ThreadLocal 对象了,于是我们将 tl 设置为空,我们希望垃圾回收器将 ThreadLocal 对象回收。释放占用的内存空间。

但是,垃圾回收器真的能将 ThreadLocal 对象回收吗?

答案是不能。

如果 Entry 对象中的 key 采用了强引用的方式指向 ThreadLocal 对象,即使将 tl 设置为 null,ThreadLocal 对象也不能被回收,因为 Entry 中的 key 以强引用仍然指向它,垃圾回收器不能将它回收,所以 ThreadLocal 就会作为垃圾一直存在于内存中,除非当前线程结束运行,map 集合对象被回收,ThreadLocal 对象也跟着被回收。但在实际业务场景中,有些线程会一直运行比如服务器线程,全年不间断运行,或是一些重要业务线程,那么 ThreadLocal 也会一直存在于内存中,如果程序中有很多的 ThreadLocal 对象都是这样不能被回收一直存在于内存中,就会造成内存泄漏。

而如果 Entry 中的 key 以弱引用的方式指向 ThreadLocal 对象,当 tl 对象设置为 null 时,ThreadLocal 对象就只剩下弱引用指向它,当垃圾回收器遇到 ThreadLocal 对象时,遇到只有弱引用指向的对象时,就可以将它回收,避免了内存泄露。

5. 如果将 tl 设置为 null,一定要将Map中的对应记录 remove

上面说到了 Entry 对象中的 key 采用弱引用的方式解决了 ThreadLocal 可能产生的内存泄漏问题,但是,产生内存泄露的不只是 ThreadLocal。

各位同学现在仔细想想,还是接着上面的第四点,我将 tl 设置为 null,tl 与 ThreadLocal 对象之间的引用就会断开,那么此时线程内部的 Map 集合保存的 ThreadLocal 那条记录的 key 也会变成 null,如下图所示。

那么现在我们就需要注意一点啦!!!

Map 集合中 Entry 的 key 变成了 null,那么此时对应的 value 值我们是访问不到的,既然访问不到,也就没有用了,value 值也会变成垃圾对象,需要被垃圾回收器回收。所以如果在程序中将 ThreadLocal 对象设置为 null 的时候,同时也需要将 Map 集合中 key 为 null 的记录全部 remove,避免 value 值无法被回收造成内存泄露。

6. 线程池中的线程使用完毕归还之前清除Map

在实际开发的过程中,我们往往不会去创建新的线程,而是使用线程池中的线程,在使用完毕之后再归还到线程池,避免线程的重复创建和销毁。

线程池的使用也随之带来了另一个问题——线程在归还到线程池之前一定要将 Map 集合清除。

原因很简单,假设现在我从线程池中拿到了一个线程来使用,并在使用的过程中向 map 集合中存放了一些 ThreadLocal 对象,我在使用完线程之后不将 Map 集合清除直接归还到了线程池中。当下次再有其它业务使用线程时,再向 Map 集合中存放 ThreadLocal 对象,如果 key 相同,就会把原来的 ThreadLocal 对象覆盖;还有一种情况就是直接从 Map 集合中取 ThreadLocal 对象,那么取出来的就是之前的,很有可能会产生错误,对业务造成影响。

7. ThreadLocal 广泛应用于线程池

如果你有阅读过线程池的源码,你就会发现线程池中广泛应用到了 ThreadLocal,并且线程池中的线程完成任务之后,首先就会清理线程内部的 Map 集合,防止对下一次的线程操作产生影响。

而且 Spring 框架中的 @Transaction 事务注解,MyBbatis框架底层的数据库连接需要用到的参数,线程池中的部分源码,都应用到了 ThreadLocal ;

除此之外,Java 中的一些集合中为了防止造成内存泄漏,也会加入 ThreadLocal 。 

ThreadLocal 也是面试中可能会问到的一个比较重要的知识点,如果能理解并牢记本篇文章,应对面试可以说绰绰有余了。

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐