深挖 ThreadLocal 底层原理?它有什么用?学会之后手撕面试官

目录

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的效果。

2. ThreadLocal 代码举例

如下代码,我都做了一些注释,应该不难理解,所以就不做过多赘述了。

public class WeakReferenceTest {
    public static void main(String[] args) {
        // 定义一个 ThreadLocal 全局变量 tl
        ThreadLocal tl = new ThreadLocal();
        // 创建一个线程
        new Thread(() -> {
            // 向 tl 对象中存放一个对象
            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 对象,但他们之间存取并不会互相影响。

深挖 ThreadLocal 底层原理?它有什么用?学会之后手撕面试官_第1张图片

3. ThreadLocal 源码分析

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

3.1 ThreadLocal 的 get 方法源码解析

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

深挖 ThreadLocal 底层原理?它有什么用?学会之后手撕面试官_第2张图片

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

深挖 ThreadLocal 底层原理?它有什么用?学会之后手撕面试官_第3张图片

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

深挖 ThreadLocal 底层原理?它有什么用?学会之后手撕面试官_第4张图片

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

深挖 ThreadLocal 底层原理?它有什么用?学会之后手撕面试官_第5张图片

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

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

3.2 ThreadLocal 的 set 方法源码解析

然后下面对 Map 做判断,如果 Map 集合不为空,说明已经存在,则将(this,value)存入其中,认真分析,此时的 this 是什么,是方法的调用者,也就是说 ThreadLocal 的对象 tl 是 Map 集合的 key,value值就是我们方法传入的 value值;

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

深挖 ThreadLocal 底层原理?它有什么用?学会之后手撕面试官_第6张图片

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

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

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

深挖 ThreadLocal 底层原理?它有什么用?学会之后手撕面试官_第7张图片

3.3 ThreadLocal 的 createMap 方法源码解析

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

createMap 源码如下所示,这里它创建了一个 ThreadLocalMap 集合

深挖 ThreadLocal 底层原理?它有什么用?学会之后手撕面试官_第8张图片

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

深挖 ThreadLocal 底层原理?它有什么用?学会之后手撕面试官_第9张图片

3.4 ThreadLocal 的 set 方法总结

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

(1)程序中我们 new 一个 ThreadLocal 对象用 tl 接收,也可以 new 多个;

(2)每个线程内部都有一个 tls 属性指向独属于自己的那个 Map 集合;

(3)当我们调用 tl 对象 set 方法的时候,实际上底层会创建一个 Entry 键值对对象,K是 set 方法的调用者 tl,V 则是我们要保存的 value 值;

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

深挖 ThreadLocal 底层原理?它有什么用?学会之后手撕面试官_第10张图片

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

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

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

我来给大家说明一下,假设现在我的程序中使用 ThreadLocal tl = new ThreadLocal() 定义了一个 ThreadLocal 对象,并且在当线程执行了 tl.set 将 tl 存储到了当前线程中,随着程序的运行,我们不再需要 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,如下图所示。

深挖 ThreadLocal 底层原理?它有什么用?学会之后手撕面试官_第11张图片

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

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 事务注解;

线程池中的部分源码,都应用到了ThreadLocal;

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

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

你可能感兴趣的:(java,开发语言)