ThreadLocal原理
关键点总结:
ThreadLocal
更像是对其他类型变量的一层包装,通过ThreadLocal
的包装使得该变量可以在线程之间隔离和当前线程全局共享。- 在
Thread
中有一个threadLocals
变量,类型为ThreadLocal.ThreadLocalMap
,ThreadLocalMap
中key
是ThreadLocal
,value
是存入的变量值。Thread
中threadLocals
由ThreadLocal
维护。- 每个线程的本地变量不是存储在
ThreadLocal
示例里边的,而是存放在调用线程的threadLocals
变量里边。LoreadLocal
类型的本地变量存放在具体的线程内存空间中。ThreadLocal
就是一个工具,通过set
方法把value
值放入调用线程的threadLocals
变量。调用**get**
方法再从当前线程的threadLocals
中拿出数据。- 如果调用线程一直不终止,那么这个本地变量会一直存放在调用线程的
thradLocals
变量里边,所以当不需要使用本地变量的时候可以通过调用ThreadLocal
的remove
方法删除本地变量- Java中的内存泄露的情况:长生命周期的对象持有短生命周期的对象的引用就很有可能发生内存泄露,尽管短生命周期对象已经不再需要,但是因为长生命周期对象持有它的引用而导致不能被回收,这就是Java中内存泄露发生的场景。
📚概述
ThreadLocal
:线程本地变量,用于解决多线程并发访问时共享变量的问题。**
📗ThreadLocal实现原理
📐存储原理
- 在
Thread
中有一个threadLocals
变量,类型为ThreadLocal.ThreadLocalMap
,ThreadLocalMap
中key
是ThreadLocal
,value
是存入的变量值。Thread
中threadLocals
由ThreadLocal
维护。- 每个线程的本地变量不是存储在
ThreadLocal
示例里边的,而是存放在调用线程的threadLocals
变量里边。LoreadLocal
类型的本地变量存放在具体的线程内存空间中。ThreadLocal
就是一个工具,通过set
方法把value
值放入调用线程的threadLocals
变量。调用**get**
方法再从当前线程的threadLocals
中拿出数据。- 如果调用线程一直不终止,那么这个本地变量会一直存放在调用线程的
thradLocals
变量里边,所以当不需要使用本地变量的时候可以通过调用ThreadLocal
的remove
方法删除本地变量
💡ThreadLocal常用方法
set(T value)
:设置线程本地变量的内容。get()
:获取线程本地变量的内容。remove()
:移除线程本地变量。注意在线程池的线程复用场景中在线程执行完毕时一定要调用remove,避免在线程被重新放入线程池中时被本地变量的旧状态仍然被保存。
Question1
:为什么get()
、remove()
方法没有入参?
Question2
:ThreadLocal
对象作为key
存在Thread
中,实际中一个线程可能会经过很多方法,如何从Thread
中获取到ThreadLocal
的数据呢?threadlocal
如何使用(没有ThreadLocal
时,Thread
能获取到存储的数据吗)?
ThreadLocal#set(T value)
方法:
public void set(T value) {// 获取当前线程Thread t = Thread.currentThread();// 获取从当前线程中获取ThreadlocalMap,如果不为空存入值(key为当前threadlocal对象实例,value为值),为空则创建一个ThreadLocaMap,key是当前的ThreadLocal ThreadLocalMap map = getMap(t);if (map != null)map.set(this, value);elsecreateMap(t, value);
}// 从Thread中获取ThreadLocalMap
ThreadLocalMap getMap(Thread t) {return t.threadLocals;
}
// 创建ThreadLocalMap 并赋值给Thread的threadLocals属性
void createMap(Thread t, T firstValue) {t.threadLocals = new ThreadLocalMap(this, firstValue);
}
ThreadLocal#get()
方法:
// 以当前ThreadLocal对象为key从Map中获取数据,数据不为空直接返回数据,数据为空,则创建一个key是当前ThreadLocal、value为null的数据存入Map中,并返回null
public T get() {Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null) {ThreadLocalMap.Entry e = map.getEntry(this);if (e != null) {@SuppressWarnings("unchecked")T result = (T)e.value;return result;}}return setInitialValue();
}
// 创建一个可以是当前ThreadLocal、value为null的数据存入Map中
private T setInitialValue() {T value = initialValue();Thread t = Thread.currentThread();// 获取线程中的threadLocals变量ThreadLocalMap map = getMap(t);if (map != null)map.set(this, value);elsecreateMap(t, value);return value;
}protected T initialValue() {return null;
}
ThreadLocal#remove()
方法:
// 从map中移除当前ThreadLocal为key的数据
public void remove() {ThreadLocalMap m = getMap(Thread.currentThread());if (m != null)m.remove(this);}
总结:
如下图所示,在每个线程内部都有一个名为threadLocals
的成员变量,该变量的类型为ThreadLocalMap
,其中key
为我们定义的ThreadLocal
变量的this
引用,value
则为我门使用set
方法设置的值。每个线程的本地变量存放在线程自己的内存变量threadLocals
中,如果当前线程一直不消亡,那么这些本地变量会一直存在,所以可能会造成内存溢出,因此使用完毕后要记得调用ThreadLocal
的remove
方法删除对应线程的threadLocals
中的本地变量。
📐使用说明
多个Thread与一个ThreadLocal
代码示例:
ThreadLocal<String> content = new ThreadLocal<>();// 线程1
Thread thread = new Thread(() -> {content.set("数据A");System.out.println(Thread.currentThread().getName() +":"+ content.get());
}, "线程1");
thread.start();// 线程2
Thread thread2 = new Thread(() -> {content.set("数据B");System.out.println(Thread.currentThread().getName() +":"+ content.get());}, "线程2");
thread2.start();
输出结果:
线程1:数据A
线程2:数据B
为什么多个线程访问同一个ThreadLocal
时,数据不会乱呢,也就是Thread2
中不会获取到Thread1
中存储的数据?
本质还是因为跟前边给出的存储原理图有关,同一个key
,数据存储在不同的Thread
中,在不同的Thread
中访问,肯定不会访问到其他Thread
的数据
同一个Thread与多个ThreadLocal
本质上是一致的,我们只需要记住,真实数据是存放在**Thread**
的**threadLocals**
变量中的即可,各个**Thread**
中数据互不干扰。
多个ThreadLocal
时,仅仅是增加了Thread
中**threadLocals**
键值对的数量。
ThreadLocal local1 = new ThreadLocal();
threadLocal.set("数据1");ThreadLocal local2 = new ThreadLocal();
threadLocal2.set("数据2");
💡ThreadLocal使用不当导致内存泄露
内存泄露问题:指程序中动态分配的堆内存由于某种原因没有被释放或者无法释放,造成系统内存的浪费,导致程序运行速度减慢或者系统奔溃等严重后果。内存泄露堆积将会导致内存溢出。
ThreadLocal
中TreadLocalMap
的Entry
继承了WeakReference
。
当一个对象被WeakReference
包装后,它就产生了一个弱引用指向它。此时即使把强引用切断,仍然有弱引用连接着。但是由于弱引用的特性,这个对象会在下次被GC
线程被直接回收。
本质不是**Entry**
是弱应用而是**Entry**
的**key**
为弱引用。
使用弱引用的原因
强引用情况:
theadlocal
为null
被回收时,在Thread
还存在的情况下,依然存在到达ThreadLocal
对象的引用链(ThreadLocalMap
中的key
),无法清除ThreadLocal
的内容,同时ThreadLocalMap
中的Value
也会保留。在使用线程池的情况下(Thread
可能一直存在)可能会出现内存泄露。
弱应用情况:
theadlocal
为null
被回收时,在Thread
还存在的情况下,在下次GC
时就会将ThreadLocal
对象清除,但是ThreadLocalMap
中的Value
还会保留,导致无法删除。在使用线程池的情况下(Thread
可能一直存在)可能会出现内存泄露。
Java
中的内存泄露的情况:长生命周期的对象持有短生命周期的对象的引用就很有可能发生内存泄露,尽管短生命周期对象已经不再需要,但是因为长生命周期对象持有它的引用而导致不能被回收,这就是Java
中内存泄露发生的场景。
避免内存泄露的方案:
- 使用完
ThreadLocal
,调用其remove
方法删除对应的Entry
数据 - 使用完
ThreadLocal
,当前Thread
也结束。【注意:该方案在线程池中不行。】
阿里巴巴开发手册中强制规定:
🎞强引用与弱引用
在Java
中有4种引用类型:强、软、弱、虚。
- 强引用不受
GC
影响,除非引用全部切断。比如Student s = new Student()
,假设当前只有s
指向Student
对象,那么当s=null
时,Student
对象会在下次GC
时被回收 - 软引用对象会在内存不足触发
GC
时被回收(适用于高速缓存) - 弱引用是每次
GC
时都回收,不论内存是否不足 - 虚引用(堆外内存,比如
zerocopy
)
📗ThreadLocal不支持继承
🗒️演示说明
public class TestThreadLocal {// 创建线程变量public static ThreadLocal<String> threadLocal = new ThreadLocal<>();public static void main(String[] args) {// 设置线程变量threadLocal.set("hello");// 启动子线程new Thread(() -> {System.out.println("child thread:" + threadLocal.get());}, "child thread").start();// 主线程输出线程变量值System.out.println("main:" + threadLocal.get());}}
同一个 ThreadLocal
变量在父线程中被设置值后,在子线程中是获取不到的,因为在子线程 thread
里面调用 get
方法时当前线程为 thread
线程,而这里调用 set
方法设置线程变量的是 main
线程,两者是不同的线程,自然子线程访问时返回 null
。那么有没有办法让子线程能访问到父线程中的值?答案是有,可以通过InheritableThreadLocal
实现
🔖解决方案——InheritableThreadLocal类
InheritableThreadLocal
可以解决父子线程中的变量共享。在最开始的类图中,可以看到Thread
类中除了有threadLoals
变量还有inheritableThreadLocals
变量。InheritableThreadLocal
继承ThreadLocal
,重写了三个方法。操作Thread
中的变量由threadLoals
变为inheritableThreadLocals
。
那么具体是如何实现的呢?
📖参考资料
- ThreadLocal原理及使用场景_小机double的博客-CSDN博客_threadlocal使用场景和原理
- ThreadLocal的内存泄露?什么原因?如何避免?_BigHong123的博客-CSDN博客_threadlocal会导致内存泄露