源码剖析: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);
)。
但是我们做异步处理都是使用线程池,线程池会复用线程会导致问题出现。遇到这种情况我们需要自己解决。