一. ThreadLocal简介
定义
ThreadLocal提供线程局部变量。这些变量与正常的变量不同,因为每一个线程在访问ThreadLocal实例的时候(通过其get或set方法)都有自己的,独立初始化的变量副本。ThreadLocal实例通常是类中的私有静态字段,使用它的目的是希望将状态(例如,用户ID或事务ID)与线程关联起来
作用
实现每一个线程都有自己专属的本地变量副本(自己用自己的变量不麻烦别人,不和其他人共享,人人有份,人各一份)。主要解决了让每个线程绑定自己的值,通过使用get()和set()方法,获取默认值或将其值更改为当前线程所存的副本的值从而避免了线程安全问题
代码示例
以售楼为例,销售员各凭本事吃饭
因为每个Thread内有自己的实例副本且该副本只由当前线程自己使用,既然其它Thread不可访问,那就不存在多线程间共享的问题。统一设置初始值,但是每个线程对这个值的修改都是各自线程互相独立的
class House {
//ThreadLocal初始化方法1,阿里手册推荐
/*ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>(){
@Override
protected Integer initialValue() {
return 0;
}
};*/
//ThreadLocal初始化方法2,ThreadLocal.withInitial
ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 0);
public void saleHouse() {
Integer value = threadLocal.get();
value++;
threadLocal.set(value);
}
}
public class ThreadLocalDemo {
public static void main(String[] args) {
//售房
House house = new House();
new Thread(() -> {
try {
for (int i = 0; i < 3; i++) {
house.saleHouse();
}
System.out.println(Thread.currentThread().getName() + " 卖出:" + house.threadLocal.get() + "套房");
} catch (Exception e) {
e.printStackTrace();
} finally {
//必须remove(),防止线程泄漏
house.threadLocal.remove();
}
}, "t1").start();
new Thread(() -> {
try {
for (int i = 0; i < 5; i++) {
house.saleHouse();
}
System.out.println(Thread.currentThread().getName() + " 卖出:" + house.threadLocal.get() + "套房");
} catch (Exception e) {
e.printStackTrace();
} finally {
house.threadLocal.remove();
}
}, "t2").start();
new Thread(() -> {
try {
for (int i = 0; i < 10; i++) {
house.saleHouse();
}
System.out.println(Thread.currentThread().getName() + " 卖出:" + house.threadLocal.get() + "套房");
} catch (Exception e) {
e.printStackTrace();
} finally {
house.threadLocal.remove();
}
}, "t3").start();
}
}
二. Thread ThreadLocal ThreadLocalMap 三者关系?
每个Thread对象维护着一个ThreadLocalMap的引用 ,ThreadLocalMap是ThreadLocal的内部类,用Entry来进行存储。调用ThreadLocal的set()方法时,实际上就是往ThreadLocalMap设置值,key是ThreadLocal对象,值Value是传递进来的对象;调用ThreadLocal的get()方法时,实际上就是往ThreadLocalMap获取值,key是ThreadLocal对象。ThreadLocal本身并不存储值,它只是自己作为一个key来让线程从ThreadLocalMap获取value,正因为这个原理,所以ThreadLocal能够实现“数据隔离”,获取当前线程的局部变量值,不受其他线程影响。
JVM内部维护了一个线程版的Map<Thread,T>(通过ThreadLocal对象的set方法,结果把ThreadLocal对象自己当做key,放进了ThreadLocalMap中),每个线程要用到这个T的时候,用当前的线程去Map里面获取,通过这样让每个线程都拥有了自己独立的变量,人手一份,竞争条件被彻底消除,在并发模式下是绝对安全的变量。
三. ThreadLocal内存泄漏问题
1. 内存泄漏
不再会被使用的对象或者变量占用的内存不能被回收,就是内存泄露。
2. 弱引用的使用
ThreadLocalMap从字面上就可以看出这是一个保存ThreadLocal对象的map(其实是以ThreadLocal为Key),不过是经过了两层包装的ThreadLocal对象:
1. 第一层包装是使用WeakReference<ThreadLocal<?>>将ThreadLocal对象变成一个弱引用的对象
2. 第二层包装是定义了一个专门的类Entry来扩展WeakReference<ThreadLocal<?>>
3. 为什么使用弱引用
如下图所示,当func1方法执行完毕后,栈帧销毁强引用 tl 也就没有了。但此时线程的ThreadLocalMap里某个entry的key引用还指向这个对象。
若这个key引用是强引用,就会导致key指向的ThreadLocal对象及v指向的对象不能被gc回收,造成内存泄漏;若这个key引用是弱引用就大概率会减少内存泄漏的问题(还有一个key为null的坑)。使用弱引用,就可以使ThreadLocal对象在方法执行完毕后顺利被回收且Entry的key引用指向为null。
4. key为null的坑
ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用引用它,那么系统gc的时候,这个ThreadLocal势必会被回收。这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话(比如正好用在线程池),这些key为null的Entry的value就会一直存在一条强引用链。Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永远无法回收,造成内存泄漏。
当然,如果当前thread运行结束,threadLocal,threadLocalMap,Entry没有引用链可达,在垃圾回收的时候都会被系统进行回收。
但在实际使用中我们有时候会用线程池去维护我们的线程,比如在Executors.newFixedThreadPool()时创建线程的时候,为了复用线程是不会结束的,所以threadLocal内存泄漏就值得我们小心。
虽然弱引用,保证了key指向的ThreadLocal对象能被及时回收,但是v指向的value对象是需要ThreadLocalMap调用get、set时发现key为null时才会去回收整个entry、value,因此弱引用不能100%保证内存不泄露。我们要在不使用某个ThreadLocal对象后,手动调用remove方法来删除它,尤其是在线程池中,不仅仅是内存泄露的问题,因为线程池中的线程是重复使用的,意味着这个线程的ThreadLocalMap对象也是重复使用的,如果我们不手动调用remove方法,那么后面的线程就有可能获取到上个线程遗留下来的value值,造成bug。
5. 解决弱引用内存泄漏
调用get,set或remove方法时,就会尝试删除key为null的entry,可以释放value对象所占用的内存。
如阿里JAVA开发手册所示,必须回收自定义的 ThreadLocal 变量,尤其在线程池场景下,线程经常会被复用,如果不清理自定义的ThreadLocal 变量,可能会影响后续业务逻辑和造成内存泄露等问题。尽量在代理中使用 try-finally 块进行回收。
四. 总结
ThreadLocal并不解决线程间共享数据的问题
ThreadLocal适用于变量在线程间隔离且在方法间共享的场景
ThreadLocal通过隐式的在不同线程内创建独立实例副本避免了实例线程安全的问题
每个线程持有一个只属于自己的专属Map并维护了ThreadLocal对象与具体实例的映射,该Map由于只被持有它的线程访问,故不存在线程安全以及锁的问题
ThreadLocalMap的Entry对ThreadLocal的引用为弱引用,避免了ThreadLocal对象无法被回收的问题都会通过expungeStaleEntry,cleanSomeSlots,replaceStaleEntry这三个方法回收键为null的Entry
对象的值(即为基体实例)以及Entry对象本身从而防止内存泄漏,属于安全加固的方法