文章目录
在多线程编程中,我们创建的变量默认可以被任何线程访问和修改。这无疑带来了数据竞争和线程安全的挑战。那么,如果想让每个线程都拥有自己的专属变量,互不干扰,该如何实现呢?
答案就是 JDK 提供的 ThreadLocal
。
一、ThreadLocal 是什么?为什么需要它?
ThreadLocal
类允许我们创建只能被当前线程读写的变量。可以将其形象地比喻为一个“存放私有物品的盒子”。每个线程进入系统时,都会得到一个属于自己的、独立的盒子。线程可以随时向自己的盒子里存放或取用物品,但无法窥探或操作其他线程的盒子。
当你创建一个 ThreadLocal
变量时,每个访问该变量的线程都会获得一个独立的、初始化的副本。这也是 ThreadLocal
名称的由来。线程可以通过 get()
方法获取自己线程的本地副本,或通过 set()
方法修改该副本的值,从而根除了线程安全问题。
举个简单的例子:假设有两个人去宝屋寻宝。如果他们共用一个袋子,必然会产生争执;但如果每个人都有一个独立的袋子,就不会有这个问题。如果将这两个人比作线程,那么 ThreadLocal
就是为他们提供独立袋子的方法。
代码示例:
public class ThreadLocalExample {
// 使用 withInitial 为每个线程提供一个初始值为 0 的副本
private static ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 0);
public static void main(String[] args) {
Runnable task = () -> {
// 1. 获取当前线程的副本值
int value = threadLocal.get();
// 2. 在副本上进行修改
value += 1;
// 3. 将修改后的值设置回当前线程的副本
threadLocal.set(value);
// 4. 再次获取并打印,验证隔离性
System.out.println(Thread.currentThread().getName() + " Value: " + threadLocal.get());
};
Thread thread1 = new Thread(task, "Thread-1");
Thread thread2 = new Thread(task, "Thread-2");
thread1.start();
thread2.start();
}
}
输出:
Thread-1 Value: 1
Thread-2 Value: 1
可以看到,Thread-1
和 Thread-2
各自对 threadLocal
的操作互不影响,都从初始值 0
变成了 1
。
二、ThreadLocal 究竟把数据存哪了?
要理解 ThreadLocal
的工作机制,我们从Thread
类的源码入手。
public class Thread implements Runnable {
//...
// 与此线程有关的 ThreadLocal 值。由 ThreadLocal 类维护
ThreadLocal.ThreadLocalMap threadLocals = null;
// 与此线程有关的 InheritableThreadLocal 值。由 InheritableThreadLocal 类维护
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
//...
}
可以看出,每个 Thread
对象内部都包含一个名为 threadLocals
的成员变量,它的类型是 ThreadLocal.ThreadLocalMap
。
ThreadLocalMap
是 ThreadLocal
的一个静态内部类,可以理解为一个定制版的 HashMap
。默认情况下 threadLocals
是 null
,只有当前线程首次调用 ThreadLocal
的 set()
或 get()
方法时,这个 Map
才会被创建。
set()方法调用过程
ThreadLocal.set(T value)
方法:
public void set(T value) {
// 1. 获取发起调用的当前线程
Thread t = Thread.currentThread();
// 2. 从当前线程对象中,获取其内部的 threadLocals 变量(即那个 Map)
ThreadLocalMap map = getMap(t);
if (map != null)
// 3a. 如果 Map 已存在,就将值存入 Map
map.set(this, value);
else
// 3b. 如果 Map 不存在,就为该线程创建一个 Map 并存入初始值
createMap(t, value);
}
// getMap(t) 的实现非常直接,就是返回线程的成员变量
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
通过源码分析,我们得出结论:
- 数据并不存储在
ThreadLocal
实例中。ThreadLocal
实例扮演的是一个“钥匙”或“入口”的角色。 - 数据真正存储在调用线程
Thread
自身的threadLocals
字段(一个ThreadLocalMap
)中。 ThreadLocalMap
的key
是ThreadLocal
实例本身,而value
才是我们想要存储的数据。
一张图看懂三者关系
Thread
、ThreadLocal
和 ThreadLocalMap
之间的关系如下图所示:
每个线程(Thread
)都有一个自己的 ThreadLocalMap
。当我们在同一个线程中声明了多个 ThreadLocal
对象(如 userThreadLocal
、traceIdThreadLocal
),这些 ThreadLocal
实例会作为不同的 key
,将它们各自的 value
存储在当前线程唯一的那个 ThreadLocalMap
中。
三、ThreadLocal 的内存泄漏陷阱
ThreadLocal
如果使用不当,会引发内存泄漏。这个问题的根源在于 ThreadLocalMap
的内部实现。
ThreadLocalMap
存储键值对的单位是其内部类 Entry
。
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k); // key 是一个弱引用
value = v; // value 是一个强引用
}
}
这里的引用关系是导致内存泄漏的关键:
- Key 是弱引用:
Entry
的key
是一个WeakReference
,它包装了ThreadLocal
实例。当一个对象只被弱引用指向时,垃圾回收器(GC)下一次运行时就会回收它。这意味着,如果外部代码不再有对ThreadLocal
实例的强引用(比如myThreadLocal = null
),那么在下一次 GC 后,ThreadLocalMap
中对应Entry
的key
就会变成null
。 - Value 是强引用:
Entry
的value
是一个Object
类型的强引用。即使key
因为弱引用被回收了,Entry
对象本身还存在于ThreadLocalMap
的table
数组中,并且它依然强引用着我们存储的value
对象。
内存泄漏就这样发生了:当一个 ThreadLocal
实例被回收(key
变为 null
),但持有它的线程却一直存活(比如线程池中的复用线程),那么这个 key
为 null
的 Entry
及其强引用的 value
将永远无法被回收,从而造成内存泄漏。
虽然 ThreadLocalMap
在执行 get()
、set()
、remove()
时会顺便清理一些 key
为 null
的 Entry
,但这种清理是被动的,不能保证及时性。
如何避免内存泄漏?
最有效、最推荐的做法是:在使用完 ThreadLocal
后,务必手动调用 remove()
方法。
remove()
方法会明确地将当前 ThreadLocal
对应的 Entry
从当前线程的 ThreadLocalMap
中移除,彻底切断引用链,避免内存泄漏。
特别是在线程池等线程会被复用的场景下,强烈建议使用 try-finally
结构来确保 remove()
的执行。
try {
myThreadLocal.set(someValue);
// ... 业务逻辑 ...
} finally {
myThreadLocal.remove(); // 保证无论如何都会被执行
}
参考链接:https://javaguide.cn/java/concurrent/java-concurrent-questions-03.html#threadlocal-%E6%9C%89%E4%BB%80%E4%B9%88%E7%94%A8