竹笋

首页 » 问答 » 常识 » 不懂ThreadLocal,面试官很难相
TUhjnbcbe - 2024/3/6 17:59:00

当多线程访问共享可变数据时,涉及到线程间同步的问题,并不是所有时候,都要用到共享数据,所以就需要线程封闭出场了。

数据都被封闭在各自的线程之中,就不需要同步,这种通过将数据封闭在线程中而避免使用同步的技术称为线程封闭。

本文主要介绍线程封闭中的其中一种体现:ThreadLocal,将会介绍什么是ThreadLocal;从ThreadLocal源码角度分析,最后介绍ThreadLocal的应用场景。

ps:下面这本书可能是最好Java并发编程书籍了,趁着活动赶紧购买,错过就只能在等下一年了

什么是ThreadLocal?

ThreadLocal是Java里一种特殊变量,它是一个线程级别变量,每个线程都有一个ThreadLocal就是每个线程都拥有了自己独立的一个变量,竞态条件被彻底消除了,在并发模式下是绝对安全的变量。

可以通过ThreadLocalTvalue=newThreadLocalT();来使用。

会自动在每一个线程上创建一个T的副本,副本之间彼此独立,互不影响,可以用ThreadLocal存储一些参数,以便在线程中多个方法中使用,用以代替方法传参的做法。

下面通过例子来了解下ThreadLocal:

首先通过staticfinal定义了一个THREAD_LOCAL变量,其中static是为了确保全局只有一个保存String对象的ThreadLocal实例;

final确保ThreadLocal的实例不可更改,防止被意外改变,导致放入的值和取出来的不一致,另外还能防止ThreadLocal的内存泄漏。上面的例子是演示在不同的线程中获取它会得到不同的结果,运行结果如下:

首先在Thread-0线程执行之前,先给THREAD_LOCAL设置为wupx,然后可以取到这个值,然后通过创建一个新的线程以后去取这个值,发现新线程取到的为null,意外着这个变量在不同线程中取到的值是不同的,不同线程之间对于ThreadLocal会有对应的副本,接着在线程Thread-0中执行对THREAD_LOCAL的修改,将值改为huxy,可以发现线程Thread-0获取的值变为了huxy,主线程依然会读取到属于它的副本数据wupx,这就是线程的封闭。

看到这里,我相信大家一定会好奇ThreadLocal是如何做到多个线程对同一对象set操作,但是get获取的值还都是每个线程set的值呢,接下来就让我们进入源码解析环节:

ThreadLocal源码解析

首先看下ThreadLocal都有哪些重要属性:

其中的HASH_INCREMENT也不是随便取的,它转化为十进制是,转换成int类型就是-,等于(√5-1)/2乘以2的32次方。(√5-1)/2就是*金分割数,近似为0.,也就是说0x61c理解为一个*金分割数乘以2的32次方,它可以保证nextHashCode生成的哈希值,均匀的分布在2的幂次方上,且小于2的32次方。

下面是javaspecialists中一篇文章对它的介绍:

Thisnumberrepresentsthegoldenratio(sqrt(5)-1)timestwotothepowerof31((sqrt(5)-1)*(2^31)).Theresultisthenagoldennumber,eitheror-.

下面用例子来证明下:

运行结果为:

921522294

可以发现元素索引值完美的散列在数组当中,并没有出现冲突。

ThreadLocalMap

除了上述属性外,还有一个重要的属性ThreadLocalMap,ThreadLocalMap是ThreadLocal的静态内部类,当一个线程有多个ThreadLocal时,需要一个容器来管理多个ThreadLocal,ThreadLocalMap的作用就是管理线程中多个ThreadLocal,源码如下:

从源码中看到ThreadLocalMap其实就是一个简单的Map结构,底层是数组,有初始化大小,也有扩容阈值大小,数组的元素是Entry,Entry的key就是ThreadLocal的引用,value是ThreadLocal的值。ThreadLocalMap解决hash冲突的方式采用的是线性探测法,如果发生冲突会继续寻找下一个空的位置。

这样的就有可能会发生内存泄漏的问题,下面让我们进行分析:

ThreadLocal内存泄漏

ThreadLocal在没有外部强引用时,发生GC时会被回收,那么ThreadLocalMap中保存的key值就变成了null,而Entry又被threadLocalMap对象引用,threadLocalMap对象又被Thread对象所引用,那么当Thread一直不终结的话,value对象就会一直存在于内存中,也就导致了内存泄漏,直至Thread被销毁后,才会被回收。

那么如何避免内存泄漏呢?

在使用完ThreadLocal变量后,需要我们手动remove掉,防止ThreadLocalMap中Entry一直保持对value的强引用,导致value不能被回收,其中remove源码如下所示:

remove方法的时序图如下所示:

remove方法是先获取到当前线程的ThreadLocalMap,并且调用了它的remove方法,从map中清理当前ThreadLocal对象关联的键值对,这样value就可以被GC回收了。

那么ThreadLocal是如何实现线程隔离的呢?

ThreadLocal的set方法

我们先去看下ThreadLocal的set方法,源码如下:

set方法的作用是把我们想要存储的value给保存进去。set方法的流程主要是:

先获取到当前线程的引用利用这个引用来获取到ThreadLocalMap如果map为空,则去创建一个ThreadLocalMap如果map不为空,就利用ThreadLocalMap的set方法将value添加到map中set方法的时序图如下所示:

其中map就是我们上面讲到的ThreadLocalMap,可以看到它是通过当前线程对象获取到的ThreadLocalMap,接下来我们看getMap方法的源代码:

getMap方法的作用主要是获取当前线程内的ThreadLocalMap对象,原来这个ThreadLocalMap是线程的一个属性,下面让我们看看Thread中的相关代码:

可以看出每个线程都有ThreadLocalMap对象,被命名为threadLocals,默认为null,所以每个线程的ThreadLocals都是隔离独享的。

调用ThreadLocalMap.set()时,会把当前threadLocal对象作为key,想要保存的对象作为value,存入map。

其中ThreadLocalMap.set()的源码如下:

相信到这里,大家应该对Thread、ThreadLocal以及ThreadLocalMap的关系有了进一步的理解,下图为三者之间的关系:

ThreadLocal的get方法

了解完set方法后,让我们看下get方法,源码如下:

get方法的主要流程为:

先获取到当前线程的引用获取当前线程内部的ThreadLocalMap如果map存在,则获取当前ThreadLocal对应的value值如果map不存在或者找不到value值,则调用setInitialValue()进行初始化get方法的时序图如下所示:

其中每个Thread的ThreadLocalMap以threadLocal作为key,保存自己线程的value副本,也就是保存在每个线程中,并没有保存在ThreadLocal对象中。

其中ThreadLocalMap.getEntry()方法的源码如下:

ThreadLocalMap的resize方法

当ThreadLocalMap中的ThreadLocal的个数超过容量阈值时,ThreadLocalMap就要开始扩容了,我们一起来看下resize的源代码:

resize方法主要是进行扩容,同时会将垃圾值标记方便GC回收,扩容后数组大小是原来数组的两倍。

ThreadLocal应用场景

ThreadLocal的特性也导致了应用场景比较广泛,主要的应用场景如下:

线程间数据隔离,各线程的ThreadLocal互不影响方便同一个线程使用某一对象,避免不必要的参数传递全链路追踪中的traceId或者流程引擎中上下文的传递一般采用ThreadLocalSpring事务管理器采用了ThreadLocalSpringMVC的RequestContextHolder的实现使用了ThreadLocal总结

本文主要从源码的角度解析了ThreadLocal,并分析了发生内存泄漏的原因,最后对它的应用场景进行了简单介绍。

欢迎留言交流讨论,原创不易,觉得文章不错,请在看转发支持一下。

1
查看完整版本: 不懂ThreadLocal,面试官很难相