为什么我学了那么久还是对ThreadLocal一脸懵逼?
如何理解ThreadLocal
全局容器这个概念相信大家都不陌生了,就是在任何地方都可以访问得到的一个容器
在一个类里面声明static的容器,可用称之为全局容器
public static final Map<String,String> SERVERS_LIST = new ConcurrentHashMap<>(16);
那ThreadLocalMap和ThreadLocal怎么理解呢?
我们同样可以把ThreadLocalMap理解为一个全局容器,它们有一个共同的特点,允许在任何地方从全局容器拿到东西,而ThreadLocal是接触到ThreadLocalMap的途径,因为我们先要先声明一个ThreadLocal,才能够对ThreadLocalMap进行操作,要将ThreadLocal作为key存到ThreadLocalMap中去
不同点在哪?ThreadLocalMap是独属于当前线程的全局容器,而上面讲的Map容器是属于所有线程的全局容器
即每个线程本身就有一个ThreadLocalMap容器,最经典的案例可以看一下SpringSecurity的ContextHolder容器,其本质就是通过ThreadLocal实现的,具体大家可以往下看
ThreadLocalMap的类结构体
每个Thread维护一个ThreadLocalMap对象,这个Map的key是ThreadLocal实例本身,value是存储的值要隔离的变量
-
每个Thread线程内部都有一个Map(ThreadLocalMap::threadlocals)
-
Map里面存储ThreadLocal对象(key)和线程的变量副本(value)
-
Thread内部的Map由ThreadLocal维护,由ThreadLocal负责向map获取和设置变量值
-
对于不同的线程,每次获取副本值时,别的线程不能获取当前线程的副本值,就形成了数据之间的隔离。
这也是为什么ThreadLocal能够做到线程隔离的原因
如果这张图还是不太清楚,可以再看看这张
看几张图可能还是不够直观 --- 源码分析
- 首先是我们从ThreadLocal中get()到我们的ThreadLocalMap
ThreadLocal<Map<String, Object>> threadLocal = new ThreadLocal<>();
Map<String, Object> map = THREAD_LOCAL.get();
// THREAD_LOCAL.get() 到底get了什么?
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();
}
// 来到 ThreadLocalMap map = getMap(t);
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
// 如果 t.threadLocals = null 那就要走到这里需要去初始化ThreadLocalMap
private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
// 初始化一个ThreadLocalMap,需要有一个初始值,初始值直接给了null
protected T initialValue() {
return null;
}
// 然后开始创建一个ThreadLocalMap
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
此时我们惊奇的发现,t.threadLocals是什么!居然就在Thread里面还是以属性存在的,点进去看源码
原来ThreadLocal是这样做线程隔离的,每个线程都具有自己单独的ThreadLocalMap容器,做到了线程隔离
public
class Thread implements Runnable {
ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
}
再来看看set方法,this作为key,即ThreadLocal作为你的key
所以一个ThreadLocal只对应一个value,注意Map的key是唯一的
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
ThreadLocal内存泄漏 - 为什么set后要remove?
内存泄漏问题:指程序中动态分配的堆内存由于某种原因没有被释放或者无法释放,造成系统内存的浪费,导致程序运行速度减慢或者系统奔溃等严重后果。内存泄漏堆积将会导致内存溢出。
ThreadLocal的内存泄漏问题一般考虑和Entry对象有关,在上面的Entry定义可以看出ThreadLocal::Entry被弱引用所修饰
既然提到强弱引用,难道说ThreadLocal内存泄漏问题就是因为强弱引用导致的吗?其实不然,我们分类来讨论一下
-
使用强引用
使用了强引用会发生什么呢?
当ThreadLocal Ref被回收了,由于在Entry使用的是强引用,在Current Thread还存在的情况下就存在着到达Entry的引用链,
无法清除掉ThreadLocal的内容,同时Entry的value也同样会被保留
也就是说,如果使用强引用可能就会出现内存泄漏的问题
-
使用弱引用
当ThreadLocal Ref被回收了,由于在Entry使用的是弱引用,因此在下次垃圾回收的时候就会将ThreadLocal对象清除,这个时候Entry中的KEY=null。但是由于ThreadLocalMap中任然存在Current Thread Ref这个强引用,因此Entry中value的值任然无法清除。还是存在内存泄漏的问题。
总结与思考
根据上面强弱引用的讨论,我们可以知道使用ThreadLocal造成内存泄漏跟强弱引用没有关系
造成内存泄漏的真正原因是:ThreadLocalMap的生命周期与Thread一致,如果不手动清除掉Entry对象的话就可能会造成内存泄漏
因此,需要我们在每次使用完后手动的remove掉Entry对象,但是我对此有几个疑问并结合了我自己的思考:
-
既然ThreadLocalMap和Thread的生命周期是一样的,那在当前线程结束后,ThreadLocalMap不是也会被销毁吗?
怎么就造成了内存泄漏了呢?线程池中的线程复用有关
当一个线程执行完毕并被正确地回收时,它内部的ThreadLocalMap实例也会随着线程一同被垃圾回收器回收,因此不会出现内存泄漏的问题。然而,在使用ThreadLocal结合线程池的情况下,可能会遇到内存泄漏的风险
在线程池中,工作线程通常会复用,而不是每次任务完成后就销毁。这意味着线程会持续存在,即使某个使用了ThreadLocal的任务已经完成了它的生命周期
-
在真实项目中是怎么用的
private static final InheritableThreadLocal<Map<String, Object>> THREAD_LOCAL = new InheritableThreadLocal<>();
项目中是用的static,那也就是说这个ThreadLocal是不会被回收的,所以注意用完就是要remove()掉
-
既然强弱引用都会导致内存泄漏,为什么用的是弱引用而不是强引用呢?
在ThreadLocalMap的set/getEntry中,会对key进行判断,如果key为null,那么value也会被设置为null,这样即使在忘记调用了remove方法,当ThreadLocal被销毁时,对应value的内容也会被清空。多一层保障!
拓展
InheritableThreadLocal
传统的ThreadLocal有什么问题吗 ?为什么又来一个InheritableThreadLocal ?
来了一个场景,我需要在父子线程中使用ThreadLocal怎么办?
即我想要在子线程能够拿到父线程中的ThreadLocalMap中的数据,可是每个线程的ThreadLocal都是线程隔离的
别的线程是拿不到其他线程的ThreadLocalMap中的内容的,InheritableThreadLocal就是解决这种问题的
TransmitThreadLocal
TransmittableThreadLocal 解决的主要问题包括:
- 跨线程传播:在同一线程池内,线程切换时能够自动地将ThreadLocal中的数据进行传递,即使这些线程并非父子关系。
- 跨进程传播:在 RPC 调用或者消息队列等分布式调用场景下,能够在服务间的远程调用过程中传递特定的线程局部变量,使得在整个调用链路中都能访问到这些上下文信息。
通过使用 TransmittableThreadLocal,开发人员可以在多线程和跨进程的复杂环境中更方便地管理和传递上下文信息,例如请求ID、用户登录信息、事务上下文等。
转载自:https://juejin.cn/post/7330054489079152681