图解分析ThreadLocal的原理与应用场景

2020-08-08  本文已影响0人  JackpotDC

ThreadLocal的介绍

ThreadLocal这个类想必大家都不陌生,直接翻译为线程本地(变量),我们经常会使用到它来保存一些线程隔离的全局的变量信息。使用ThreadLocal维护变量时,每个线程都会获得该线程独享一份变量副本。
ThreadLocal比较像是DNF中的一个地下城副本,而每个线程像是每个进入DNF副本中的玩家。各个线程进入副本后都是比较隔离的,不会互相干扰,这一特性在多线程的某些场景下十分适用。

龙龙的奇妙比喻--ThreadLocal

ThreadLocal介于全局变量与局部变量之间的生命周期

ThreadLocal将变量的使用范围恰当的保存到了全局变量和局部变量之间。

ThreadLocal常见的使用场景

笔者经常使用ThreadLocal的场景有:

ThreadLocal的实现原理

ThreadLocal.get()

ThreadLocal实现结构以及执行的过程如下图所示。


ThreadLocal.get()的过程

ThreadLocal的几个关键词。

  1. 哈希,每个线程内部独立维护着一个ThreadLocalMap,这是一个Entry[]数组,通过对ThreadLocal进行hash(具体细节读者可以从源码了解)获取到Entry的下标
  2. 哈希冲突的解决办法采用了开放地址法,对于如图所示hash冲突的情况则下标挪一位再找(哈希冲突的三种解决办法:HashMap常用的拉链法、开放地址法、再哈希法,感兴趣的读者可以自行搜索:哈希冲突的解决办法)。ThreadLocal通常存放的数据量不会特别大,并且使用开放地址法(或叫开放寻址法)相对于拉链法而言节省了存储指针的空间
  3. WeakReference弱引用,ThreadLocalMap中对于ThreadLocal的引用使用了弱引用,弱引用的作用是当该引用是该对象的唯一一个引用时,不阻碍GC的回收,下面将展开讨论下ThreadLocal中弱引用与内存泄漏的问题

ThreadLocalMap中的弱引用与使用注意

如前文所述,ThreadLocalMap其实是一个ThreadLocal --> value的映射,具体的实现关系如下图

ThreaLocal清理的过程
当线程中使用的ThreadLocal置为null的时候,ThreadLocalMap中的弱引用作为最后一个指向ThreadLocal的引用,发生GC的时候直接被回收掉,但是这时Entry中的value不会被回收
ThreadLocal的set/get/remove方法中在遇到key==null的节点时(被称为stale腐烂节点),会进行清理等处理逻辑。
  1. 如果Thread1执行完销毁了,那么ThreadLocalMap会整个销毁,也就不会有内存泄漏的问题了
  2. 如果Thread1长期存在,并且一直在创建新的ThreadLocal,并且从来没有执行过set/get/remove方法是有一定可能导致内存泄漏的
  3. 一般情况下我们会使用线程池,这样会在执行完后表现为线程结束,实际上线程只是回到了池子中等待下次调度的时候再次使用,这种情况时ThreadLocal是会被复用的,假如前面的使用场景中我们使用ThreadLocal保存了traceId,如果线程执行完没有进行回收并且下次执行的时候没有重新设置traceId的话,那么在打印日志的时候又会打印前一次的traceId,这样也会导致很多逻辑上的错误

因此,必须在使用了ThreadLocal的线程执行完后finally中调用threadLocal.remove(),或者如果ThreadLocal<HashMap>的话则调用threadlocal.get().remove()清空HashMap

ThreadLocal的复制

在ThreadLocal的使用中,我们经常会需要创建子线程,希望子线程能够继承父线程的ThreadLocal,还是以traceid的使用场景为例,我们创建了子线程来并发处理耗时的逻辑,并且希望子线程中也能如实的打印当前请求的traceid,但是普通的ThreadLocal在创建新线程后信息会完全丢失,笔者曾经在这里踩到过坑。

所以就需要一种方案来复制ThreadLocal到子线程:

  1. 先将ThreadLocal的内容保存在堆中,再子线程中将堆中的内容复制过来


    ThreadLocal的复制
  2. InheritableThreadLocal(线程池无效),原理是子线程是通过在父线程中通过调用new Thread()方法来创建子线程,Thread#init方法在Thread的构造方法中被调用。在init方法中拷贝父线程数据到子线程中。但是注意!我们现在一般都会使用线程池创建新线程,这种时候所谓的创建新线程只是复用了线程池中已有的线程,并不会调用new Thread()方法,因此使用InheritableThreadLocal往往是没效果的
    InheritableThreadLocal的原理
  3. 阿里巴巴开源了TransmittableThreadLocal,据说可以解决2中的问题,这个后面我们可以再看下,笔者一般只是使用1的方法基本就可以解决子线程ThreadLocal复制的问题

reference

[1] ThreadLocal-hash冲突与内存泄漏
[2] ThreadLocal面试攻略:吃透它的每一个细节和设计原理
[3] 面试官:小伙子,听说你看过ThreadLocal源码?(万字图文深度解析ThreadLocal)

上一篇 下一篇

猜你喜欢

热点阅读