在理解时,不宜将 ThreadLocal 理解为横跨若干线程、存储并管理不同线程某些成员值的容器
ThreadLocal 本身没有存储功能,提供存储功能的是 ThreadLocalMapThreadLocalMap 并不是共用的,每个线程持有自己的 ThreadLocalMapThreadLocal 仅相当于对各个线程各自的 ThreadLocalMap 的统一操作面板| 方法 | 作用 | 备注 | 示例 |
|---|---|---|---|
get() | 获取当前线程的变量值 | 获取变量值的快照 | |
set() | 设置当前线程的变量值 | ||
remove() | 移除当前线程的变量值 | 使用后应该在 finally 中移除,否则尤其是线程池场景容易造成内存泄漏 | |
withInitial() | 初始化当前线程的变量值 | 静态方法,默认的初始值是 null | ThreadLocal.withInitial(()->0) |
ThreadLocal<Integer> count = ThreadLocal.withInitial(()->0);
public static void main(String[] args) {
ThreadLocalDemo demo = new ThreadLocalDemo();
for(int i=0;i<5;i++){
new Thread(()->{
try {
demo.count.set(demo.count.get()+ new Random().nextInt(10));
System.out.println(Thread.currentThread().getName()+ " | " + demo.count.get());
} finally {
demo.count.remove(); // 用完即删
}
},String.valueOf(i)).start();
}
}
Thread、ThreadLocal、ThreadLocalMap每个 Thread 中都有一个 ThreadLocalMap

ThreadLocalMap 由 ThreadLocalMap.Entry 组成

ThreadLocalMap.Entry 是 ThreadLocal 的内部类,用于存储 ThreadLocal 的弱引用 和 value 的映射
或者说,ThreadLocalMap.Entry 就是一种带有 value 的弱引用 WeakReference

通过 ThreadLocal 存取值时,其实是对 ThreadLocalMap(里的 ThreadLocalMap.Entry ) 进行操作
以 get() 为例
ThreadLocalMapThreadLocalMap.Entry

总结:
Thread 持有各自独立的 ThreadLocalMapThreadLocalMap 中存储了当前线程的各个线程本地化变量ThreadLocal 的弱引用和 value 的映射ThreadLocal 本身并不存储值,存值的是ThreadLocalMapThreadLocal 在不同 ThreadLocalMap 中映射不同的值
示例
注意各个对象的 id
ThreadLocal<Integer> count = ThreadLocal.withInitial(()->0);
ThreadLocal<Integer> num = ThreadLocal.withInitial(()->0);
public static void main(String[] args) {
ThreadLocalDemo demo = new ThreadLocalDemo();
new Thread(()->{
demo.count.set(10);
demo.num.set(20);
System.out.println(demo.count.get()+demo.num.get());
}).start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) { e.printStackTrace(); }
new Thread(()->{
demo.count.set(1);
demo.num.set(2);
System.out.println(demo.count.get()+demo.num.get());
}).start();
}



为什么会有内存泄漏
先暂时忽略 ThreadLocalMap.Entry 上的弱引用,只考虑几个类的实例之间的引用关系,如下图所示
ThreadLocalMapThreadLocalMap 可能持有多个 ThreadLocalMap.EntryThreadLocalMap.Entry 又引用各自的 ThreadLocal 和 值ThreadLocalMap.Entry 上引用的 ThreadLocal 又可能是同一个
当 线程销毁 时,线程与它栈帧中的 ThreadLocalMap、Entry 都会回收
若 Entry 引用的 ThreadLocal 和 值没有额外引用(比如其他线程,或声明着 ThreadLocal 的对象),则也会被回收
但在 线程复用的场景(比如线程池),线程不会被销毁,会出现两种 内存泄漏
ThreadLocal 被 ThreadLocalMap.Entry 持有导致ThreadLocalMap.Entry 导致ThreadLocal 被 ThreadLocalMap.Entry 持有导致的内存泄漏
线程复用场景中,上述各个对象都不会被回收
这使得即使声明着 ThreadLocal 的对象都已经被回收了,ThreadLocal 也不不能被回收
因为它依然可以存在于各个线程的 ThreadLocalMap 中
JDK 为了解决这个问题,将 Entry 设计成弱引用 key,如下图

Entry 对 ThreadLocal 的引用被定义为弱引用,当发生 GC 时,Entry 的 key 会被置空(null)
若此时,原本作为 key 的 ThreadLocal 未被其他有效引用所引用的 ThreadLocal 对象会被回收,完美的解决了此问题
key==null 的 ThreadLocalMap.Entry 导致的内存泄漏
但是,key==null 的 Entry 依然被 ThreadLocalMap 持有
这相当于标记了这个 Entry 为腐败Entry(stale entry),变得不可访问,但它和它引用的 value 依然占用内存
JDK 为了解决这个问题,为 ThreadLocal 提供了两种方案
自动回收,即 replaceStaleEntry() / expungeStaleEntry() 方法
这些方法会在 set() / get() 的过程中发现 null 值的 key 后自动调用以清理腐败 Entry
这可以解决大部分内存泄漏问题,但不能保证100%(比如 value 存了个很大的东西,然后在没有 set() / get() 过 )

手动回收,remove() 方法,如下图
remove() 方法会直接对 key 进行 expungeStaleEntry()
使用完 ThreadLocal 后,应该调用此方法以做到实时清理 ThreadLocalMap ,见上文的 例子


题外话:为什么 Entry 的 value 不能通过弱引用解决
因为 value 有可能出现只被 Entry 引用的情况,这回导致在还没有使用完时,因 GC 丢失值
Entry 的 key 是因为它至少被声明 ThreadLocal 的对象强引用着,对象销毁才意味着它们已经没用了
总结
使用 ThreadLocal 造成内存泄漏的场景通常是因为
ThreadLocal 的 remove()ThreadLocal
withInitial() 方法初始化 ThreadLocal
set() 直接 get() 后容易出现空指针异常remove() 方法
ThreadLocal 中 set() 共享对象(虽然线程隔离了,但隔离中的还是同一个东西的投影)
ThreadLocal 并不能完美的隔离所有变量ThreadLocal 变相实现线程安全set() 同一个 HashMap