在理解时,不宜将 ThreadLocal
理解为横跨若干线程、存储并管理不同线程某些成员值的容器
ThreadLocal
本身没有存储功能,提供存储功能的是 ThreadLocalMap
ThreadLocalMap
并不是共用的,每个线程持有自己的 ThreadLocalMap
ThreadLocal
仅相当于对各个线程各自的 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()
为例
ThreadLocalMap
ThreadLocalMap.Entry
总结:
Thread
持有各自独立的 ThreadLocalMap
ThreadLocalMap
中存储了当前线程的各个线程本地化变量ThreadLocal
的弱引用和 value 的映射ThreadLocal
本身并不存储值,存值的是ThreadLocalMap
ThreadLocal
在不同 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
上的弱引用,只考虑几个类的实例之间的引用关系,如下图所示
ThreadLocalMap
ThreadLocalMap
可能持有多个 ThreadLocalMap.Entry
ThreadLocalMap.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