java 小册子之 ThreadLocal

240 阅读5分钟

小册子系列的第三篇文章来啦。虽然上一篇反响平平,但这打击不了我要坚持下去的决心。来吧,展示。

java 并发工具,ThreadLocal

笔者在这事先说明,本文采用的是 jdk11 的版本进行分析。还是老样子,先把可以回答的点给各位看官列一下:

  1. ThreadLocal 基本介绍
  2. ThreadLocal 底层实现
  3. ThreadLocal 的应用及注意事项

magazine-unlock-01-2.3.3172-64CC0842558ABCB69F3F0.jpg

多想和你住在这,过个桥,就能见到你

ThreadLocal 基本介绍

要想了解一个东西是啥,最直接的方式就是看它的出厂说明,没人比创造它的人更了解它了。

This class provides thread-local variables

上面的英文就是摘自这个类的注释,翻译过来就是:该类提供线程局部变量。用人话说就是,如果某个变量仅仅在单个线程中被访问的,那么就可以将这个变量存储在这个类中。大家平时在写代码时,如果碰到某个变量不会被多个线程访问,但它本身又需要在多个方法中被使用时会怎么写?最直观的方法就是把它放在方法的参数中,有多少个方法,就写多少次。这种方法当然阔以,但不美观,这段代码可能会被你的领导或同事拿出来反复鞭尸。为了避免屎山的出现,ThreadLocal 踏着七彩祥云来了。ThreadLocal 可以将这个变量以一种更为优雅的方式进行传递,需要存储值时,简单的调下 set 方法即可,而取值时,自然就是 get 方法了。

ThreadLocal 底层实现

talk is cheap show me the code.

threadlocal.set.png

java.lang.ThreadLocal#set 方法,简约,但不简单

整个方法的流程异常清晰明了。就是获取当前线程,接着根据当前线程获取 ThreadLocalMap 对象,如果能获取到,就把值扔进去;如果获取不到,就创建一个,然后扔进去。在上面的第一个问题中有提到线程局部变量这个概念,方法的局部变量大家能理解,变量的作用域局限于方法中;线程的局部变量也一样,变量的作用域就在整个线程中。那为啥可以横跨这么大呢?秘密就在于这个 ThreadLocalMap 对象是跟当前线程绑定的,是一种唇亡齿寒的关系,这个线程用到的变量都放在里面,作用域当然就横跨整个线程了。那么 ThreadLocalMap 又是怎样的一种结构呢?先从名字上看,猜测应该类似于哈希表这种 k-v 结构的存储,点进去瞅瞅:

threadlocalmap.png

table 数组就是存储数据的地方,ThreadLocal 并不实际存储数据

ThreadLocalMap#set.png

java.lang.ThreadLocal.ThreadLocalMap#set 方法

上图方法的流程也比较清晰,根据传入的 ThreadLocal 计算出 value 要放在 table 数组中的哪个位置上,也就是变量 i(key.threadLocalHashCode & (len-1),可能会被问)。如果该位置没东西,那么皆大欢喜,直接往里放就好了;如果该位置上已经有了其他东西,说明产生了哈希冲突,就从 i 位置开始往后找位置存(也叫线性探测法)。最后就是更新数组中的元素数量,根据条件判断是否需要对数组进行扩容。数组中每个位置都是一个 Entry 对象,里面存的是 ThreadLocal 对象的弱引用和 value 。整个过程和哈希表类似,ThreadLocal 相当于是 key,要放入的值就是 value,一样可能发生哈希冲突和扩容。 但不同的是 ThreadLocalMap#set 方法还调用了 replaceStaleEntry、cleanSomeSlots 两个方法,这两个方法是为了避免出现内存泄漏所设计的。关于这个问题,笔者会在第三个问题中解释,请各位稍安勿躁。

ThreadLocal#get.png

java.lang.ThreadLocal#get 方法

既然存是存到 ThreadLocalMap 中,那取值自然是从里面拿了。

ThreadLocalMap#getEntry.png

java.lang.ThreadLocal.ThreadLocalMap#getEntry 方法

没啥好分析的,怎么放的,就怎么取。

magazine-unlock-02-2.3.11302-9619A7A2A63786E5AE8C.jpg

在落日前,见到你

ThreadLocal 的应用及注意事项

ThreadLocal 在很多地方都会用到,平时的编码及项目使用的框架中都能见到。譬如在框架中看到什么 xxxContextHolder 这种类,大概率都是借助 ThreadLocal 实现的。ThreadLocal 的使用虽然简单,但也要注意其中的坑。这个坑如果不注意,轻则在半夜嘿咻时被迫转移注意力,重则排队领高温补贴(也就是要去搬砖了)。这个坑就是上面提到的内存泄露

现在的应用可以说都使用到了线程池技术,这也就意味着,线程创建之后就很难被销毁,ThreadLocalMap 也就一直被引用着不能被 gc 回收掉,其中存储线程局部变量的 table 数组也就不能被回收,一直占用着堆内存。当我们自己 new 出来的 ThreadLocal 对象被 gc 回收后,由于 Entry 对象中持有的是 ThreadLocal 的弱引用,而弱引用的回收原则是应收尽收,因此当发现 Entry 对象关联的 ThreadLocal 被回收时,就可以释放掉这部分内存。

jdk 官方也想到了这个问题,所以在 ThreadLocalMap#set 方法中会调用 replaceStaleEntry、cleanSomeSlots 这两个方法,检查一下有没可以回收的。各位读者可以点进去看下这两个方法的具体实现。

那为啥不能用强引用呢?道理也很简单,因为是强引用,jvm 宁可 oom 都不会去动它的。当应用中 new 出来的 ThreadLocal 不再使用时(方法已经执行结束),由于 Entry 对象中还持有 ThreadLocal 的强引用以及放入的值,这部分内存始终无法被回收掉,并且应用中已经没办法再使用了,这时就发生了内存泄露。因此弱引用可以在一定程度上避免这种情况的发生。

我们平时在使用时,最好的方式就是记得在最后调用一下 java.lang.ThreadLocal#remove 方法释放掉Entry 对象占用的内存。

以上就是本篇文章的全部内容啦,我不知道有没人看,但我还是会继续下去。如果有同样喜欢分享和写作的朋友也可以私信我,大家相互扶持,共同进步。