首先我们先明确一点,这里我们谈论的是比如 线程池中的核心线程 的情况,而不是普通的run完就销毁的线程。后面会继续说明为什么。
假设线程run()这样:
public void run() {
ThreadLocal<String> threadLocal = new ThreadLocal<>();
threadLocal.set("abc");
}
关系是:
threadLocal 变量只是你线程中的一个局部变量,在线程的栈帧中,它指向了一个堆上的 ThreadLocal 实例。
1、ThreadLocal是一个Java工具类,当我们
new ThreadLocal<>()
就只是new了一个这个工具类,其他的什么都没有发生。并不是每个线程都有一个它,不要混淆了。
每个线程都有一个的是ThreadLocalMap,它是Thread类的成员,只是这个map里面存的键值对,其中key的类型恰好就是ThreadLocal。
2、当我们执行
threadLocal.set("abc");
①ThreadLocal 会获取当前线程(Thread.currentThread());
②拿到该线程中的 ThreadLocalMap;
③然后往当前线程的ThreadLocalMap里,新增一个键值对(也可能是修改),key就是刚刚new的这个threadLocal对象,value就是设置的“abc”。
补充一句,这个ThreadLocalMap是懒加载的,并不是Thread出生就有一个,是第一次执行ThreadLocal.set()的时候才会出生。
首先,内存泄漏,并不是顾名思义的说这个线程想独有的内存给别的线程偷偷抢走了,而是,内存不能被回收,就是内存泄露。
为了避免混淆,我们把上面的代码改个名字
public void run() {
ThreadLocal<String> ikun = new ThreadLocal<>();
threadLocal.set("abc");
}
当我们new了一个ThreadLocal对象ikun之后,实际保存是这样的:ikun是一个引用,指向实际的ThreadLocal对象,ikun保存在这个线程的栈里,而实际的ThreadLocal对象数据保存在堆里,并且ikun是强引用这个对象。而ThreadLocalMap也在堆里,而它里面的新创建的键值对,key是一个引用,引用了ThreadLocal对象,并且key是弱引用这个对象,而value就是"abc"不管它,不过要注意value是对"abc"的强引用。
我们把这段话剔出来骨头就是:
栈里的 ikun 变量是对堆中那个 ThreadLocal 实例的强引用;
ThreadLocalMap 的键值对中的 key 是对同一个 ThreadLocal 实例的弱引用。
两个引用都指向同一个堆上的 ThreadLocal 实例,只是引用类型不同:栈变量是强引用,key 是弱引用。
也就是:
引用来源 | 引用类型 | 指向 | 作用 |
---|---|---|---|
栈上的 ikun | 强引用 | 堆中的 ThreadLocal 实例 | 保持实例存活,确保它不被 GC |
ThreadLocalMap.Entry | 弱引用 (WeakReference) | 同一个 ThreadLocal 实例 | 允许 GC 回收无用 ThreadLocal 实例,防止内存泄漏 |
ikun要强引用(其实就是普通引用)很合理,因为ikun要用ThreadLocal,要是弱引用,还没用完就直接给GC回收了,那就抽象了。换句话说,ikun就是一个普通的变量引用,就和 Object o = new Object(); 的o没区别,这个o当然也是对Object对象的强引用。
但是为什么ThreadLocalMap的键值对的key要对ThreadLocal用弱引用呢?
——实际上就是为了防止或者说缓解内存泄漏而设计的。
因为设计成弱引用就是这样的:
当线程的run()运行结束之后,该方法中的局部变量(包括 ikun)全部出栈,ikun就走了,这个强引用就消失了;
这个时候,堆上的 ThreadLocal 实例只有 线程内部 ThreadLocalMap 键值对的Key的 弱引用 指向它;
JVM GC 发现该 ThreadLocal 实例只有弱引用,没有强引用,就会将其回收;
回收后,ThreadLocalMap 键值对中的 key 就会变成 null(弱引用被清理后),线程后续调用 ThreadLocal 的 get 方法时,会检测到 key 为 null 的键值对,进而触发清理,释放对应的 value(强引用),避免内存泄漏。
想想要是key对ThreadLocal是强引用,那么ikun出栈之后,ThreadLocal就一直不能被回收,这内存泄漏就很严重了。
因此,这样设计可以避免线程长时间持有已失效的 ThreadLocal 实例,导致其关联的值无法被回收;
即使用户忘记调用threadLocal.remove(),也能通过弱引用 + 后续清理机制来防止内存泄漏。
不过这里有个问题,线程都run完了,谁来调用ThreadLocal 的get方法以触发清理?
这个问题就是开头说明的,其实这个说法是针对线程池中的核心线程的。
如果只是普通线程,比如说web项目中的一次请求这样的普通线程,那么当Thread 对象终止后,JVM 的垃圾回收器会回收线程对象和它持有的所有成员变量,包括线程的 ThreadLocalMap,因为线程结束意味着该线程对象不再被 JVM 持有,也没有任何活动线程引用它;
因此,普通线程执行完 run() 后,ThreadLocalMap 及其中的所有键值对也会随着线程对象一起被清理,不会造成内存泄漏。
但是如果是线程池中的线程,这一次run()完了之后,线程并不会直接被销毁,尤其是核心线程,不会被销毁,一直等着下一个任务呢,所以相当于给了他二次上线的机会,再去调用ThreadLocal的 get 方法以触发清理。
所以可以说threadlocal的内存泄露只是暂时的,因为普通线程销毁了就全回收了,而线程池的线程在下一次被调用的时候运行set、get方法的时候就会触发清理了。
但是如果这个核心线程一直没有机会二进宫,就一直不会被清理。
threadlocalmap的key本身就是对threadlocal的弱引用,弱引用不会直接被回收掉吗?
如果有这样的问题,说明你搞混淆了,key是弱引用,引用的是ThreadLocal,要回收是ThreadLocal,而不是key。
ThreadLocal身上还有ikun的强引用,所以ikun出栈前ThreadLocal不会被回收。
这里提一嘴,GC回收判断对象是否是垃圾的 引用计数法:当一个对象身上背负的引用数到0的时候,GC才能清理它,弱引用不计在内。
当然,他还有可达性分析法:从 GC Roots 开始向下搜索,搜索走过的路径称为引用链,当一个对象和 GC Roots 之间没有任何引用链相连时,则证明此对象是不可用的垃圾。