在复习juc的线程时发现,ThreadLocal
是经常被提问到的一个知识点 ,如果感觉有用,请给我一个赞吧Ciallo~(∠・ω< )⌒★。
这篇文章主要从以下几个角度来分析理解:(1)ThreadLocal是什么;(2)ThreadLocal怎么用;(3)ThreadLocal源码分析;(4)ThreadLocal内存泄漏问题
以下源码均基于jdk1.8
。
1、ThreadLocal是什么?
从名称上看,ThreadLocal 可以理解为“线程本地变量”,也就是说,ThreadLocal 中存储的是当前线程所专有的变量。该变量对其他线程是不可见的,从而实现了线程间的数据隔离。ThreadLocal 为每个线程创建了变量的副本,使得每个线程都能独立访问自己的那份数据。
虽然字面意思很直观,但在实际使用中,ThreadLocal 的理解和应用并不简单,因此也常常作为面试中的高频考点之一。其使用场景非常丰富,常见的包括:
-
跨层传递对象:在多层架构中,通过 ThreadLocal 可避免在各层间显式传递共享对象,打破层与层之间的参数传递约束,简化代码结构。
-
线程数据隔离:每个线程独享数据副本,有效避免线程安全问题,常用于多线程并发场景下的数据隔离。
-
事务管理:可用于存储与当前线程相关的事务状态信息,便于在执行链路中统一管理事务生命周期。
-
数据库连接与会话管理:在 Web 应用或 ORM 框架中,常使用 ThreadLocal 管理当前线程的数据库连接(Connection)或会话(Session),确保操作的线程安全性与上下文一致性。
2、ThreadLocal怎么用?
public class ThreadLocalTest {
public static void main(String[] args) {
ThreadLocal<String> local = new ThreadLocal<>();
IntStream.range(0, 10).forEach(i -> new Thread(() -> {
local.set(Thread.currentThread().getName() + ":" + i);
System.out.println("线程:" + Thread.currentThread().getName() + ",local:" + local.get());
}).start());
}
}
输出结果:
线程:Thread-0,local:Thread-0:0
线程:Thread-1,local:Thread-1:1
线程:Thread-2,local:Thread-2:2
线程:Thread-3,local:Thread-3:3
线程:Thread-4,local:Thread-4:4
线程:Thread-5,local:Thread-5:5
线程:Thread-6,local:Thread-6:6
线程:Thread-7,local:Thread-7:7
线程:Thread-8,local:Thread-8:8
线程:Thread-9,local:Thread-9:9
从结果可以看到,每一个线程都有自己的local 值,这就是TheadLocal的基本使用 。
3、ThreadLocal源码分析
在JDK8之后每一个线程都会维护一个ThreadLoaclMap,这个Map是一个哈希散列结构,如下图所示,每一个元素(Entry)都是一个键值对,key为ThreadLocal,Value为存储的数据,也就是set()方法存储的内容。
图片参考链接:图文详解ThreadLocal:原理、结构与内存泄漏解析-CSDN博客
但是在早期并不是这样的,早期的JDK中都是由ThreadLocal来维护这样的一个Map,里面的key则是Thread,就像下图这样
在并发编程中,线程数通常远大于ThreadLocal的数量。当线程销毁时,JDK8的方案在内存管理上更具优势:它只需删除对应的ThreadLocalMap即可释放内存。相比之下,JDK8之前的实现中,Thread仅作为Map的一个键值存在,线程销毁后会导致对应的Map空间无法有效回收,从而造成内存利用率低下,接下来查看源码分析。
(1)set方法
/**
* Sets the current thread's copy of this thread-local variable
* to the specified value. Most subclasses will have no need to
* override this method, relying solely on the {@link #initialValue}
* method to set the values of thread-locals.
*
* @param value the value to be stored in the current thread's copy of
* this thread-local.
*/
public void set(T value) {
//首先获取当前线程对象
Thread t = Thread.currentThread();
//获取线程中变量 ThreadLocal.ThreadLocalMap
ThreadLocalMap map = getMap(t);
//如果不为空,
if (map != null)
map.set(this, value);
else
//如果为空,初始化该线程对象的map变量,其中key 为当前的threadlocal 变量
createMap(t, value);
}
/**
* Create the map associated with a ThreadLocal. Overridden in
* InheritableThreadLocal.
*
* @param t the current thread
* @param firstValue value for the initial entry of the map
*/
//初始化线程内部变量 threadLocals ,key 为当前 threadlocal
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
/**
* Construct a new map initially containing (firstKey, firstValue).
* ThreadLocalMaps are constructed lazily, so we only create
* one when we have at least one entry to put in it.
*/
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
ThreadLocalMap 是 ThreadLocal 的静态内部类,其内部定义了 Entry 类用于存储数据。Entry 继承自弱引用,并以 ThreadLocal 实例作为键(key),用户设置的值作为值(value)。每个线程内部都维护着一个 ThreadLocal.ThreadLocalMap 变量,所有数据的存取操作都是通过这个容器来完成的。
(2)get方法
/**
* Returns the value in the current thread's copy of this
* thread-local variable. If the variable has no value for the
* current thread, it is first initialized to the value returned
* by an invocation of the {@link #initialValue} method.
*
* @return the current thread's value of this thread-local
*/
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 的 key 来获取对应的 value 值。
4、ThreadLocal 内存泄漏问题
首先是内存管理中的两个关键概念:
-
内存溢出(Memory Overflow) 指系统无法为程序提供足够的内存空间来满足其申请需求。
-
内存泄漏(Memory Leak) 指程序在运行过程中动态分配的内存,由于程序逻辑缺陷或其他原因未能及时释放,导致这部分内存无法被系统回收利用。内存泄漏会逐渐消耗系统资源,不仅降低程序运行效率,严重时还会引发程序崩溃。值得注意的是,持续的内存泄漏最终可能导致内存溢出。
下图是ThreadLocal相关的内存结构图,在栈区中有threadLocal对象和当前线程对象,分别指向堆区真正存储的类对象,这俩个指向都是强引用。在堆区中当前线程肯定是只有自己的Map的信息的,而Map中又存储着一个个的Entry节点;在Entry节点中每一个Key都是ThreadLocal的实例,同时Value又指向了真正的存储的数据位置,以上便是下图的引用关系。
那么所谓的内存泄漏,其实就是指的Entry这块内存不能正确释放。
有人可能会猜测出现内存泄漏是因为Entry中使用了弱引用的key(如下所示继承关系中的WeakReference),但这种理解其实是不对的,那我们假设key强弱引用的情况。
首先是key强引用的情况,在业务代码中使用完ThreadLocal后,栈区对堆区的引用关系会被回收。然而,由于ThreadLocal的Key是强引用,导致堆区中的ThreadLocal对象无法被回收。此时,Key仍然指向ThreadLocal,且由于当前线程尚未结束,线程对ThreadLocalMap的强引用关系依然存在。这种情况下,虽然栈上的引用已经消失,我们无法访问堆中的ThreadLocal对象,进而无法访问对应的Entry。但由于Entry仍被ThreadLocalMap引用,导致其既无法被访问也无法被回收,最终造成Entry的内存泄漏问题。
而key采用弱引用,当业务代码使用完毕后,key会被垃圾回收机制(GC)回收并置为null。然而,由于当前线程仍在运行,Entry对象仍被Map所引用,因此不会被GC回收。此时,key虽然为null,但对应的value却既无法访问也无法被回收,最终导致value发生内存泄漏。
总结
通过上述分析可以发现,无论是Entry中的Key采用强引用还是弱引用,都可能引发内存泄漏问题。区别在于:强引用会导致Entry本身的内存泄漏,而弱引用则会造成Value的内存泄漏。这两种情况具有以下共同特征:
- 未手动删除Entry
- 当前线程持续运行
要避免内存泄漏问题,关键在于使用完ThreadLocal后及时调用remove()方法,清除对应的Entry。此外,由于ThreadLocalMap是Thread的成员属性,其生命周期与线程绑定。当线程结束时,ThreadLocalMap随之消亡,Entry也就失去了引用,从而从根本上解决了内存泄漏问题。
综上所述,ThreadLocal内存泄漏的根本原因在于:ThreadLocalMap的生命周期与线程一致,若未手动删除对应的Key,就会导致内存泄漏。