ThreadLocal
简介
- ThreadLocal提供线程局部变量。这些变量与正常的变量不同,因为每一个线程在访问ThreadLocal实例的时候(通过其get或set方法)都有自己的、独立初始化的变量副本。ThreadLocal实例通常是类中的私有静态字段,使用它的目的是希望将状态(例如,用户ID或事务ID)与线程关联起来。
- 实现每一个线程都有自己专属的本地变量副本(自己用自己的变量不麻烦别人,不和其他人共享,人人有份,人各一份),主要解决了让每个线程绑定自己的值,通过使用get()和set()方法,获取默认值或将其值更改为当前线程所存的副本的值从而避免了线程安全问题。
常用API
如何使用?
- 初始化值,避免空指针
- 建议使用static修饰
- 必须使用try,finally块进行删除
/**
* ThreadLocal
* 用完删除,防止线程复用,导致内存泄漏
*/
public class ThreadLocalTest02 {
public static void main(String[] args) {
ExecutorService threadPool = Executors.newFixedThreadPool(3);//创建线程池
try {
Data data = new Data();
for (int i = 0; i <6 ; i++) {
threadPool.submit(()->{
try {
Integer b = data.threadLocal.get();
data.add();
Integer a = data.threadLocal.get();
System.out.println(Thread.currentThread().getName()+"\t"+b+"\t"+a);
} finally {
data.threadLocal.remove();//用完删除,防止线程复用,导致内存泄漏
}
});
}
} finally {
threadPool.shutdown();//关闭线程池
}
}
}
class Data{
int data=0;
//初始化值,匿名内部类写法
// ThreadLocal<Integer> threadLocal=new ThreadLocal<Integer>(){
// @Override
// protected Integer initialValue() {
// return 0;
// }
// };
//初始化值,推荐写法
ThreadLocal<Integer> threadLocal=ThreadLocal.withInitial(()->0);
public void add(){
threadLocal.set(threadLocal.get()+1);
}
}
源码分析
Thread,ThreadLocal,ThreadLocalMap 关系
内存泄露问题
什么是内存泄漏?
不再会被使用的对象或者变量占用的内存不能被回收,就是内存泄露。
内存泄漏原因
【强制】必须回收自定义的ThreadLocal变量,尤其在线程池场景下,线程经常会被复用,如果不清理自定义的 ThreadLocal变量,可能会影响后续业务逻辑和造成内存泄露等问题。
ThreadLocalMap为什么要用弱引用key? key为null的entry如何处理?
- 当function1方法执行完毕后,栈帧销毁强引用 tl 也就没有了。
- 但此时线程的ThreadLocalMap里某个entry的key引用还指向这个对象,若这个key引用是强引用,就会导致key指向的ThreadLocal对象及v指向的对象不能被gc回收,造成内存泄漏;
- 若这个key引用是弱引用就大概率会减少内存泄漏的问题。使用弱引用,就可以使ThreadLocal对象在方法执行完毕后顺利被回收且Entry的key引用指向为null。
key为null的entry,原理解析
-
ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用引用他,那么系统gc的时候,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话(比如正好用在线程池),这些key为null的Entry的value就会一直存在一条强引用链。
-
虽然弱引用,保证了key指向的ThreadLocal对象能被及时回收,但是v指向的value对象是需要ThreadLocalMap调用get、set时发现key为null时才会去回收整个entry、value,因此弱引用不能100%保证内存不泄露。我们要在不使用某个ThreadLocal对象后,手动调用remoev方法来删除它,尤其是在线程池中,不仅仅是内存泄露的问题,因为线程池中的线程是重复使用的,意味着这个线程的ThreadLocalMap对象也是重复使用的,如果我们不手动调用remove方法,那么后面的线程就有可能获取到上个线程遗留下来的value值,造成bug。
如何解决key为null的entry
set,get,remove方法中,在threadLocal的生命周期里,针对threadLocal存在的内存泄漏的问题, 都会通过expungeStaleEntry,cleanSomeSlots,replaceStaleEntry这三个方法清理掉key为null的脏entry。
总结
- ThreadLocal 适用于变量在线程间隔离且在方法间共享的场景
- ThreadLocal 通过隐式的在不同线程内创建独立实例副本避免了实例线程安全的问题
- 每个线程持有一个只属于自己的专属Map并维护了ThreadLocal对象与具体实例的映射,该Map由于只被持有它的线程访问,故不存在线程安全以及锁的问题
- ThreadLocalMap的Entry对ThreadLocal的引用为弱引用,避免了ThreadLocal对象无法被回收的问题都会通过expungeStaleEntry,cleanSomeSlots,replaceStaleEntry这三个方法回收键为 null的 Entry 对象的值(即为具体实例)以及 Entry 对象本身从而防止内存泄漏,属于安全加固的方法结