源码剖析:ThreadLocal

ThreadLocal用来提供线程级别变量,变量只对当前线程可见。相比于使用锁控制共享变量访问顺序的解决方案,ThreadLocal通过空间换时间的策略,每个线程都有属于自己的线程私有变量,很好地规避了线程竞争的问题。
首先回答两个问题:
- 什么是
ThreadLocal?
ThreadLocal顾名思义可以理解为线程本地变量。也就是说如果定义了一个ThreadLocal,每个线程对这个ThreadLocal中数据的读写是线程隔离的,互相不影响的,它提供了一种将可变数据通过每个线程持有一份私有的独立副本从而实现线程隔离的机制。
ThreadLocal的大致实现思路?
Thread类中有一个ThreadLocal.ThreadLocalMap类型的属性threadLocals,也就是说每个线程拥有一个自己的ThreadLocalMap。ThreadLocalMap是一种针对ThreadLocal定制化的HashMap,它的Key为ThreadLocal对象(实际是ThreadLocal的弱引用),Value为储存的线程私有数据。每个线程往ThreadLocal中添加值时,都会向线程专属的ThreadLocalMap里存,读数据也是以某个ThreadLocal作为引用,在自己的Map中寻找对应的Key,从而实现线程隔离。
下面从源码出发进行分析。
1. 源码剖析
1.1 Thread类
Thread类中包含threadLocals和inheritableThreadLocals两个属性,它们都是ThreadLocal.ThreadLocalMap类型。可以发现这两个属性默认初始化为null,只有当调用ThreadLocal的set或get方法时才会创建实例对象。
1.2 ThreadLocalMap类
ThreadLocalMap是ThreadLocal的内部类,是定制化的HashMap,其也是使用Entry封装K-V存储数据的,不同的是ThreadLocalMap的Entry的Key只能是ThreadLocal类型,并且是一个「弱引用」(Java中的引用介绍过Java中的四种引用)。
为什么
ThreadLocalMap的Key要采用「弱引用」?
1.3 ThreadLocal类
方法:

常用API有get()、set(T)、remove()。
set
源码:
1 | public void set(T value) { |
可以发现,ThreadLocal中的set(v)方法实际上调用了ThreadLocalMap中的set(this, v)方法,Entry的Key为当前ThreadLocal对象。
get
源码:
1 | public T get() { |
ThreadLocal类的get()方法的执行流程为:1. 拿到当前线程ThreadLocalMap类型的的私有属性threadLocals;2. 以当前ThreadLocal对象作为threadLocals的Map的Key,拿到并范围对应的Value。
remove
源码:
1 | public void remove() { |
ThreadLocal类中的remove()方法是删除当前线程中threadLocals属性Key为当前ThreadLocal对象的Entry。
小结
每个线程内部都有一个名为threadLocals类型为ThreadLocalMap的成员变量,其中key为我们定义的ThreadLocal变量的this引用,value则为我们执行set方法设置的值。
- 第一次操作线程的
ThreadLocalMap属性时,会初始化一个ThreadLocal.ThreadLocalMap对象,set(v)会存入以参数为value的K/V数据;get()会存入以null为value的K/V数据。 ThreadLocalMap存值操作的入口为ThreadLocal.set(v)方法,并以当前ThreadLocal对象为key,参数v为value。ThreadLocalMap取值操作的入口为ThreadLocal.get()方法,key为当前ThreadLocal对象。
2. 代码实践
代码:
1 | public class ThreadLocalTest { |
运行结果:

说明:
- 定义了两个
ThreadLocal对象:countThreadLocal和nameThreadLocal - 线程一先执行,调用
ThreadLocal.set(v)方法,此时为初次操作ThreadLocal,因此会给线程一的成员变量threadLocals进行初始化并给ThreadLocalMap对象中插入两个Entry<K,V>:<countThreadLocal, 1>和<nameThreadLocal, alise>;注意此时只对线程一的threadLocals进行初始化和赋值操作,线程二的threadLocals仍为null - 线程二在给
ThreadLocal赋值前执行ThreadLocal.get()方法,此时会对线程二的成员变量threadLocals进行初始化,并插入两对默认Entry值:<countThreadLocal, null>和<nameThreadLocal, null> - 线程二调用
ThreadLocal.set(v)方法,此时threadLocals中已经有了两个Entry,调用ThreadLocalMap.set(this, v)方法后,线程二threadLocals中的两个Entry变为:<countThreadLocal, 2>和<nameThreadLocal, bob>,调用ThreadLocal中的get()方法会执行ThreadLocalMap.get(this),其中this为当前ThreadLocal对象
从上面的过程我们可以发现,两个线程都有自己私有的threadLocals对象,在进行ThreadLocal操作时两个线程的数据互相隔离、互不干扰,从而有效解决了线程同步问题。
3. ThreadLocal问题
3.1 ThreadLocal内存泄漏问题
ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用来引用它的话,那么在系统GC的时候,这个ThreadLocal会被回收,这样一来,ThreadLocalMap中会出现key为null的Entry,就无法访问这些key为null的Entry的value,如果当前线程迟迟不结束的话,那些key为null的Entry的value就会存在一条强引用链:Thread Ref->Thread->ThreadLocalMap->Entry->value,那这些value将一直都无法被回收,就会造成「内存泄漏」。
其实,在ThreadLocalMap的设计中已经考虑到了这种情况,并加上了一些防护措施:在ThreadLocal的get()、set()、remove()的时候会清除ThreadLocalMap中key为null的所有Entry。为了代码更加规范,建议我们使用完ThreadLocal后最好手动调用remove()。
3.2 ThreadLocalMap的key为何采用「弱引用」?
当存在线程复用的场景(如线程池),一个线程的寿命很长,大对象长期不被回收会影响系统运行效率与安全。下面举例说明:
举例:三个线程中的每个线程的ThreadLocalMap的其中一个Entry中的key使用的是同一个ThreadLocal类型变量的地址,都指向了ThreadLocal1;
此时假设是强引用:多个线程依赖同一个ThreadLocal1,此时线程1的ThreadLocal使用结束,想要释放其内存,但由于强引用(因为还有其他线程指向ThreadLocal1),这就导致线程1持有的ThreadLocalMap中key为ThreadLocal1的Entry所占有的内存无法释放;如果采用弱引用的话,就仍可将其释放。
我们知道,ThreadLocalMap的生命周期基本和Thread的生命周期一样,当前线程如果没有终止,那么ThreadLocalMap始终不会被GC回收。对比使用强引用,弱引用可以保证不会因为大量key的积累而导致OOM,而对应的value可以通过get()、set()、remove()在下一次调用时清除。可见,内存泄漏的根源不是「弱引用」,而是ThreadLocalMap的生命周期和Thread一样长,造成内存积累。
3.3 ThreadLocal无法给子线程共享父线程的线程副本数据
异步场景下无法给子线程共享父线程的线程副本数据,可以通过 InheritableThreadLocal 类解决这个问题。
它的原理就是子线程是通过在父线程中调用 new Thread() 创建的,在 Thread 的构造方法中调用了 Thread的init 方法,在 init 方法中父线程数据会复制到子线程(ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);)。
但是我们做异步处理都是使用线程池,线程池会复用线程会导致问题出现。遇到这种情况我们需要自己解决。