ThreadLocal提供线程局部变量。这些变量与正常的变量不同,因为每一个线程在访问ThreadLocal实例的时候(通过其get或set方法)都有自己的、独立初始化的变量副本。ThreadLocal实例通常是类中的私有静态字段,使用它的目的是希望将状态(例如,用户ID或事务ID)与线程关联起来。
阿里规范:
【强制】必须回收自定义的ThreadLocal变量,尤其在线程池场景下,线程经常会被复用,如果不清理自定义的ThreadLocal变量,可能会影响后续业务逻辑和造成内存泄露等问题。尽量在代码中使用try-finally块进行回收
public class Temp {
ThreadLocal<Integer> threadLocalField = ThreadLocal.withInitial(() -> 0);
public void add() {
threadLocalField.set(1 + threadLocalField.get());
}
public static void main(String[] args) throws InterruptedException {
Temp temp = new Temp();
ExecutorService executorService = Executors.newFixedThreadPool(3);
for (int i = 0; i < 10; i++) {
executorService.submit(() -> {
try {
Integer beforeInt = temp.threadLocalField.get();
temp.add();
Integer afterInt = temp.threadLocalField.get();
System.out.println(Thread.currentThread().getName()+"\tbeforeInt:" + beforeInt + "\tafterInt:" + afterInt);
} finally {
// temp.threadLocalField.remove();
}
});
}
executorService.shutdown();
}
}
不加temp.threadLocalField.remove(),输出:业务逻辑受到了污染
pool-1-thread-2 beforeInt:0 afterInt:1
pool-1-thread-1 beforeInt:0 afterInt:1
pool-1-thread-3 beforeInt:0 afterInt:1
pool-1-thread-2 beforeInt:1 afterInt:2
pool-1-thread-1 beforeInt:1 afterInt:2
pool-1-thread-3 beforeInt:1 afterInt:2
pool-1-thread-2 beforeInt:2 afterInt:3
pool-1-thread-3 beforeInt:2 afterInt:3
pool-1-thread-1 beforeInt:2 afterInt:3
pool-1-thread-2 beforeInt:3 afterInt:4
加了temp.threadLocalField.remove()正确输出
pool-1-thread-1 beforeInt:0 afterInt:1
pool-1-thread-3 beforeInt:0 afterInt:1
pool-1-thread-2 beforeInt:0 afterInt:1
pool-1-thread-1 beforeInt:0 afterInt:1
pool-1-thread-3 beforeInt:0 afterInt:1
pool-1-thread-2 beforeInt:0 afterInt:1
pool-1-thread-3 beforeInt:0 afterInt:1
pool-1-thread-1 beforeInt:0 afterInt:1
pool-1-thread-2 beforeInt:0 afterInt:1
pool-1-thread-3 beforeInt:0 afterInt:1
近似的可以理解为:
ThreadLocalMap从字面上就可以看出这是一个保存ThreadLocal对象的map(其实是以ThreadLocal为Key),不过是经过了两层包的ThreadLocal对象:
JVM内部维护了一个线程版的Map
ThredLocal是一个壳子,真正的存储结构是ThreadLocal里有ThreadLocalMap这么个内部类,每个Thread对象维护着一个ThreadLocalMap的引用,ThreadLocalMap是ThreadLocal的内部类,用Entry来进行存储。
public static void main(String[] args) {
ThreadLocal<String> t1 = new ThreadLocal<>(); // line1
t1.set("老铁"); // line2
t1.get(); // line3
}
line1新建了一个ThreadLocal对象,t1是强引用指向这个对象
line2调用set()方法后新建一个Entry,通过源码可知Entry对象里的key是弱引用指向这个对象
当main()方法执行完毕后,栈帧销毁强引用t1也就没有了。但此时线程的ThreadLocalMap里某个entry的key引用还指向这个对象若这个key引用是强引用,就会导致key指向的ThreadLocal对象及v指向的对象不能被gc回收,造成内存泄漏;
若这个key引用是弱引用就大概率会减少内存泄漏的问题(还有一个key为null的雷,第2个坑后面讲)。使用弱引用,就可以使ThreadLocal对象在方法执行完毕后顺利被回收且Entry的key引用指向为null
当我们为threadLocal变量赋值,实际上就是当前的Entry(threadLocal实例为key,值为value)往这个threadLocalMap中存放。Entry中的key是弱引用,当threadLocal外部强引用被置为null(t1=null),那么系统 GC 的时候,根据可达性分析,这个threadLocal实例就没有任何一条链路能够引用到它,这个ThreadLocal势必会被回收。这样一来,ThreadLocaMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value。如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链: Thread Ref>Thread -> ThreaLocalMap ->Entry-value永远无法回收,造成内存泄漏
当然,如果当前thread运行结束,threadLocal,threadLocalMap,Entry没有引用链可达,在垃圾回收的时候都会被系统进行回收
但在实际使用中我们大部分会用线程池去维护我们的线程,比如在Executors.newFixedThreadPool()时创建线程的时候,为了复用,线程是不会结束的,所以threadLocal内存泄漏就值得我们小心
最后分析方法,在set(),get(),remove()方法中,在threadLocal的生命周期里,针对threadLocal存在的内存泄露的问题,都会通过expungeStaleEntry,cleanSomeSlots,replaceStaleEntry这三个方法清理掉key为null的脏entry。故使用threadLocal时应尽量在代码中使用try-finally块调用remove进行回收
public class Temp {
static ThreadLocal<Integer> threadLocalField = ThreadLocal.withInitial(() -> 0);
public void add() {
threadLocalField.set(1 + threadLocalField.get());
}
public static void main(String[] args) throws InterruptedException {
Temp temp = new Temp();
ExecutorService executorService = Executors.newFixedThreadPool(3);
for (int i = 0; i < 10; i++) {
executorService.submit(() -> {
try {
Integer beforeInt = threadLocalField.get();
temp.add();
Integer afterInt = threadLocalField.get();
System.out.println(Thread.currentThread().getName()+"\tbeforeInt:" + beforeInt + "\tafterInt:" + afterInt);
} finally {
threadLocalField.remove();
}
});
}
executorService.shutdown();
}
}
ThreadLocal 并不解决线程间共享数据的问题
ThreadLocal 适用于变量在线程间隔离且在方法间共享的场景
ThreadLocal 通过隐式的在不同线程内创建独立实例副本避免了实例线程安全的问题
每个线程持有一个只属于自己的专属Map并维护了ThreadLocal对象与具体实例的映射该Map由于只被持有它的线程访问,故不存在线程安全以及锁的问题
ThreadLocalMap的Entry对ThreadLocal的引用为弱引用,避免了ThreadLocal对象无法被回收的问题
都会通过expungeStaleEntry,cleanSomeSlots,replaceStaleEntry这三个方法回收键为 null 的 Entry对象的值(即为具体实例)以及 Entry 对象本身从而防止内存泄漏,属于安全加固的方法