欢迎关注公众号 【11来了】(文章末尾即可扫码关注) ,持续 中间件源码、系统设计、面试进阶相关内容
在我后台回复 「资料」 可领取 编程高频电子书!
在我后台回复「面试」可领取 30w+ 字的硬核面试笔记!
感谢你的关注!
面试:了解 ThreadLocal 内存泄漏需要满足的 2 个条件吗?
什么是 ThreadLocal?
ThreadLocal 用于存储线程本地的变量,如果创建了一个 ThreadLocal 变量,在多线程环境下访问这个变量的时候,每个线程都会在自己线程的本地内存中创建一份变量的副本,从而起到 线程隔离 的作用
Thread、ThreadLocal、ThreadLocalMap 之间的关系
每一个Thread
对象均含有一个ThreadLocalMap
类型的成员变量threadLocals
,它存储本线程所有的 ThreadLocal 对象及其对应的值
ThreadLocalMap
由一个个的Entry<key,value>
对象构成,Entry继承自weakReference<ThreadLocal<?>>
,一个Entry
由ThreadLocal
对象和Object
构成
- Entry 的 key 是ThreadLocal对象,并且是一个弱引用。当指向key的强引用消失后,该key就会被垃圾收集器回收
- Entry 的 value 是对应的变量值,Object 对象
当执行set方法时,ThreadLocal首先会获取当前线程 Thread 对象,然后获取当前线程的ThreadLocalMap对象,再以当前ThreadLocal对象作为key,设置对应的 value。
由于每一条线程均含有各自私有的 ThreadLocalMap 对象,这些容器相互独立互不影响,因此不会存在线程安全性问题,从而也就无需使用同步机制来保证多条线程访问容器的互斥性
ThreadLocal 使用场景
1、在进行对象跨层传递的时候,使用ThreadLocal可以避免多次传送,打破层次间的约束。
即如果一个User对象需要从Controller层传到Service层再传到Dao层,那么把User放在ThreadLocal中,每次使用ThreadLocal来进行获取即可
2、线程间数据隔离
3、进行事务操作,用于存储线程事务信息
4、数据库连接,Session会话管理
ThreadLocal 的内存泄漏问题
先说一下什么情况下会发生内存泄漏,需要满足 2 个条件:
- 线程内部的 ThreadLocalMap 存储的数据一直未被清理
- 线程持续存活(线程处在线程池中),导致线程内部的 ThreadLocalMap 对象一直未被回收
接下来说一下什么时候,会符合上边的两个条件,首先对于 第一个条件 来说,有两种情况:ThreadLocal 定义为局部变量、ThreadLocal 定义为全局静态变量
ThreadLocal 被定义为局部变量
当 ThreadLocal 被定义为方法中的 局部变量 ,那么当线程进入该方法的时候,就会将 ThreadLocal 的引用给加载到线程的 栈 中
如下图所示,在线程栈 Stack 中,有两个变量,ThreadLocalRef 和 CurrentThreadRef,分别指向了声明的局部变量 ThreadLocal ,以及当前执行的线程内部的 ThreadLocalMap 变量
当线程执行完该方法之后,就会将该方法局部变量从栈中删除,因此Stack 线程栈中的 ThreadLocalRef 变量就会被弹出栈,因此 ThreadLocal 变量的强引用消失了,那么 ThreadLocal 变量只有 Entry 中的 key 对他引用,并且还是弱引用,因此这个 ThreadLocal 变量会被垃圾回收器给回收掉,导致 Entry 中的 key 为 null,而 value 还指向了对 Object 的强引用,因此 value 还一直存在 ThreadLocalMap 变量中,如下图:
此时可以看到,ThreadLocalMap 内部 Entry 的 key(ThreadLocal)为 null,因此无法通过 key 去访问到这个 value,导致这个 value 一直无法被回收
因此 JDK 设计者在设计 ThreadLocal 时还添加了清除 ThreadLocalMap 中 key 为 null 的 value,避免内存泄漏,这是在设计时为了避免内存泄漏而采取的措施,而我们使用的时候要保持良好的编程规范,也要手动去 remove,避免内存泄露的发生
ThreadLocal 被定义为全局静态变量
如果定义 ThreadLocal 为 private static final
,那么这个 ThreadLocal 就会在常量池中存储,而不是存储在堆中,因此 ThreadLocal 并不会被回收,也就不会出现 ThreadLocalMap 的 Entry 中 key == null 的情况
这时候要考虑的问题是当前线程在使用完 ThreadLocal 之后要主动 remove,避免数据一直存储在对应的 ThreadLocalMap 中,从而出现脏数据以及内存泄漏
接下来说一下发生内存泄漏需要满足的第二个条件:线程持续存活
在梳理第一个条件(ThreadLocalMap 中的数据未被清理)时,有两种情况,一种是 ThreadLocal 被定义为局部变量,另一种是 ThreadLocal 被定义为全局静态变量
-
当 ThreadLocal 被定义为局部变量时,会出现 ThreadLocalMap 中 key == null 的情况,在 JDK 内部会主动清理 key == null 的value,因此这种情况不会出现内存泄漏
-
当 ThreadLocal 被定义为全局静态变量时,此时 ThreadLocalMap 内部的 key 永远不会被回收,因此如果使用之后不手动 remove 对应变量,就会导致对应的值一直存活在当前线程中,如果此时再满足第二个条件 线程持续存活 ,就会导致对应的值一直不会被回收,出现内存泄漏
当使用 线程池 执行任务时, 核心线程会一直存活 ,就会导致该线程内部的 ThreadLocalMap 变量不会清理,从而导致对应的数据一直在内存中存活, 出现内存泄漏问题 ,因此在使用 ThreadLocal 是一定要遵守正确的使用规范,避免出现内存泄漏
ThreadLocal 使用规范
ThreadLocal 正确的使用方法:
- 将 ThreadLocal 变量定义成 private static final,这样就一直存在 ThreadLocal 的强引用,也能保证任何时候都能通过 ThreadLocal 的访问到 Entry 的 value 值,进而清除掉
- 每次使用完 ThreadLocal 都调用它的 remove() 方法清除数据
下面给出 ThreadLocal 的用法:
public class ThreadLocalExample {
private static final ThreadLocal<Integer> counter = new ThreadLocal<Integer>() {
@Override
protected Integer initialValue() {
return 0;
}
};
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
try {
int value = counter.get(); // 获取当前线程的副本值
counter.set(value + 1); // 修改副本值
System.out.println("Thread " + Thread.currentThread().getName() + " value: " + counter.get());
} finally {
// 手动移除
counter.remove(); // 在线程结束时移除变量
}
});
Thread t2 = new Thread(() -> {
try {
int value = counter.get();
counter.set(value + 1);
System.out.println("Thread " + Thread.currentThread().getName() + " value: " + counter.get());
} finally {
// 手动移除
counter.remove();
}
});
t1.start();
t2.start();
}
}