ThreadLocal的双刃剑:内存泄漏的挑战与对策ThreadLocal相当于私人储物空间,但是你想想,你的储物空间上
引言
前文我说过
ThreadLocal
相当于私人储物空间,但是你想想,你的储物空间上面还背了个山,你还怎么行动,直接就像大圣压被在五指山下了,啥也干不了。内存泄漏就是这样子的,只要你不注意,你就挂了。
一、ThreadLocal的工作原理
1.1 ThreadLocal的存储机制
ThreadLocal
提供了线程局部变量,使得每个使用该变量的线程都有独立的变量副本,避免了多线程间的数据共享问题。在Java中,ThreadLocal
的存储机制主要依赖于Thread
类内部的ThreadLocalMap
(成员变量)。
每个线程(Thread
对象)内部都有一个ThreadLocalMap
类型的成员变量,它是一个自定义的哈希映射。这个映射表将ThreadLocal对象作为键(Key),将线程局部变量的值作为值(Value)。当线程访问通过ThreadLocal
定义的局部变量时,它会在自己的ThreadLocalMap
中查找对应的值。
1.2 ThreadLocalMap的结构和作用
ThreadLocalMap
是ThreadLocal
类的一个内部静态类,是一个定制的哈希映射,仅适用于维护线程本地值。不会将任何操作导出到 ThreadLocal
类之外。该类是包私有的,允许在类 Thread
中声明字段。为了帮助处理非常大且长期存在的使用情况,哈希表条目使用弱引用作为键。但是,由于不使用引用队列,因此仅当表开始空间不足时才保证删除陈旧条目。
1.2.1 结构特点
-
键(Key) :
ThreadLocalMap
的键是ThreadLocal
对象的弱引用,这有助于防止内存泄漏,因为当没有强引用指向ThreadLocal
对象时,它将被垃圾回收。 -
值(Value) :值是线程局部变量的实际数据,每个线程通过
ThreadLocal
存储的变量值都存储在各自线程的ThreadLocalMap
中。
注意:ThreadLocalMap
没有采用软引用或弱引用来引用键,这与WeakHashMap
不同,因为ThreadLocalMap
的生命周期与线程相同,不需要自动回收。
1.2.2 作用
-
提供线程安全的局部变量存储,每个线程可以访问自己的局部变量,而不会影响到其他线程。
-
通过将变量存储在每个线程的
ThreadLocalMap
中,避免了同步操作,提高了访问效率。 -
由于每个线程都有自己的
ThreadLocalMap
,因此ThreadLocal
提供了一种避免使用同步的线程局部存储方式。 -
线程局部变量的生命周期与拥有它的线程的生命周期相同。当线程结束时,存储在
ThreadLocalMap
中的线程局部变量也会随之被垃圾回收器回收,除非这些变量被外部强引用所引用。
二、内存泄漏的原因
生命周期:当通过
ThreadLocal
的set
方法设置变量值时,会在当前线程的ThreadLocalMap
中创建一个键值对。当线程执行完毕,如果没有外部强引用指向这些局部变量,那么随着线程的结束,这些局部变量将会被垃圾回收。
如果
ThreadLocal
对象被垃圾回收,但是线程仍然存活,那么这个ThreadLocal
对象所关联的线程局部变量可能会成为内存泄漏的源头,因为ThreadLocalMap
中的Entry
对象仍然持有对这个ThreadLocal
对象的强引用。
通过上面的生命周期,大家就可以看出内存泄漏的核心原因就是没有及时有效的去清理;为了避免内存泄漏,通常建议在不再需要ThreadLocal
变量时,通过调用ThreadLocal
的remove
方法来手动清除当前线程的ThreadLocalMap
中对应的键值对。这样可以确保及时回收不再使用的局部变量,防止内存泄漏。
2.1 内存泄漏场景
常见的内存泄漏场景有以下几点:
三、内存泄漏的影响与检测方法
对于内存泄漏和检测,是一个非常大的命题,这里不多做描述,下面的脑图是一些简单的整理,可参考。
四、预防和解决ThreadLocal内存泄漏的策略
4.1 正确使用ThreadLocal的实践
-
定义为静态字段:通常将
ThreadLocal
变量定义为静态字段,以确保其在整个应用程序的生命周期内有效。 -
及时清理:在不再需要
ThreadLocal
变量时,应该调用remove()
方法来清除当前线程的ThreadLocalMap
中的对应条目。
4.2 使用try-finally块确保清理
在代码中使用try-finally
块来确保无论操作是否成功,都会执行remove()
方法清理资源。例如:
public static void main(String[] args) {
ThreadLocal<String> threadLocal = new ThreadLocal<>();
try {
threadLocal.set("你猜我想干啥");
// 业务代码
String temp = threadLocal.get();
System.out.println(temp);
// 业务代码
} finally {
threadLocal.remove();
}
}
当然,实际应用场景要根据情况来,上面的只是案例。
4.3 干货:使用WeakReference来避免强引用
ThreadLocalMap
的键(Key)是对ThreadLocal
对象的弱引用,这意味着当没有强引用指向ThreadLocal
对象时,它将被垃圾回收器回收。但是,ThreadLocalMap
中的值(Value)是强引用,如果ThreadLocal
对象被回收,而线程(如线程池中的线程)仍然存活,那么ThreadLocalMap
中的值将无法访问,但不会被垃圾回收器回收,从而导致内存泄漏。
为了避免这种情况,可以自定义ThreadLocal
子类,在initialValue
方法中返回一个WeakReference
对象:
import java.lang.ref.WeakReference;
public class CustomThreadLocal extends ThreadLocal<WeakReference<String>> {
/**
* 自定义初始值
*
* @return 初始值(注意这里的返回类型由上面的泛型决定)
*/
@Override
protected WeakReference<String> initialValue() {
return new WeakReference<String>("PENDING");
}
}
这样,即使ThreadLocal
对象被垃圾回收,ThreadLocalMap
中的值也会因为包装在WeakReference
中而被允许回收。
五、总结
内存泄漏是每个程序员都会面临的问题,我们要有敬畏之心,但是不能畏之如虎;要充分的认识问题,解析问题,方能解决问题。
希望本文对您有所帮助。如果有任何错误或建议,请随时指正和提出。
同时,如果您觉得这篇文章有价值,请考虑点赞和收藏。这将激励我进一步改进和创作更多有用的内容。
感谢您的支持和理解!
转载自:https://juejin.cn/post/7419984211144425512