假设你在一家大公司工作,公司有个特殊规定:每个员工可以申请 "私人储物柜",用来存放自己的私人物品(比如钥匙、笔记本),而且只有自己能看到和使用自己的柜子。
-
公司就像我们的程序
-
每个员工就像一个线程(Thread)
-
储物柜系统就像ThreadLocal
-
员工存的物品就是我们要隔离的变量
比如,员工 A 存了一本笔记本,员工 B 存了一把钥匙,两人互不干扰 ------ 就算用了同一个 "储物柜系统"(同一个 ThreadLocal 实例),也看不到对方的东西。
为什么需要这样的系统?
在多线程场景中,如果多个线程共享一个变量,很容易出现 "混乱"(线程安全问题)。比如两个线程同时修改一个变量,可能导致结果错误。
ThreadLocal 的作用就是:让每个线程拥有变量的 "私人副本" ,线程间互不干扰,从根本上避免了共享变量的冲突。
代码体验:ThreadLocal 的基本使用
先看一段简单代码,感受下 ThreadLocal 的效果:
java
public class ThreadLocalDemo {
// 创建一个ThreadLocal实例(相当于"储物柜系统")
private static ThreadLocal<String> userThreadLocal = new ThreadLocal<>();
public static void main(String[] args) {
// 线程1:存"张三"
new Thread(() -> {
userThreadLocal.set("张三");
System.out.println("线程1的用户:" + userThreadLocal.get()); // 输出:张三
}).start();
// 线程2:存"李四"
new Thread(() -> {
userThreadLocal.set("李四");
System.out.println("线程2的用户:" + userThreadLocal.get()); // 输出:李四
}).start();
}
}
运行后会发现:两个线程用同一个userThreadLocal
,但各自存的值互不影响。这就是 "私人储物柜" 的效果。
源码拆解:ThreadLocal 是如何实现的?
核心原理可以总结为一句话:每个线程(Thread)内部都维护了一个 "储物柜列表"(ThreadLocalMap),ThreadLocal 实例只是这个列表的 "钥匙" 。
我们逐步看源码:
1. 线程的 "储物柜列表":Thread 类中的 threadLocals
每个 Thread 对象里,都有一个特殊的变量threadLocals
,它的类型是ThreadLocal.ThreadLocalMap
(可以理解为 "储物柜列表"):
java
public class Thread implements Runnable {
// 线程的"储物柜列表",由ThreadLocal维护
ThreadLocal.ThreadLocalMap threadLocals = null;
}
这个threadLocals
默认是null
,就像员工刚入职时,还没有分配储物柜。
2. ThreadLocal 的 set () 方法:存东西到自己的柜子
当我们调用threadLocal.set(value)
时,到底发生了什么?
看ThreadLocal
的set
方法源码:
java
public void set(T value) {
// 1. 获取当前线程(相当于"当前员工")
Thread t = Thread.currentThread();
// 2. 拿到当前线程的"储物柜列表"(threadLocals)
ThreadLocalMap map = getMap(t);
if (map != null) {
// 3. 如果列表存在,用当前ThreadLocal实例作为"钥匙",存值
map.set(this, value);
} else {
// 4. 如果列表不存在,为线程创建一个新的"储物柜列表"
createMap(t, value);
}
}
// 获取线程的threadLocals(储物柜列表)
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
// 为线程创建新的储物柜列表,并存入第一个值
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
流程拆解:
-
先找到 "当前员工"(当前线程)
-
查看他有没有 "储物柜列表"(threadLocals)
- 有:用当前 ThreadLocal 作为 "钥匙",把值存到对应的柜子里
- 没有:先给员工分配一个新的 "储物柜列表",再存值
3. ThreadLocal 的 get () 方法:取自己柜子里的东西
调用threadLocal.get()
时,流程类似:
java
public T get() {
// 1. 获取当前线程
Thread t = Thread.currentThread();
// 2. 拿到线程的"储物柜列表"
ThreadLocalMap map = getMap(t);
if (map != null) {
// 3. 用当前ThreadLocal作为钥匙,找对应的柜子
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
// 4. 找到就返回值
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
// 5. 如果没找到,返回默认值(一般是null)
return setInitialValue();
}
简单说:用当前 ThreadLocal 当钥匙,去当前线程的 "储物柜列表" 里找对应的值。
4. 核心:ThreadLocalMap------ 线程的 "储物柜列表"
ThreadLocalMap
是 ThreadLocal 的内部类,本质是一个自定义的哈希表(类似 HashMap,但更简单),用来存储 "钥匙 - 值" 对:
-
钥匙:ThreadLocal 实例(每个 ThreadLocal 对应一个唯一的 key)
-
值:我们要存的变量(线程私有副本)
ThreadLocalMap
里的 "柜子" 是Entry
对象:
java
static class Entry extends WeakReference<ThreadLocal<?>> {
// 我们存的变量值
Object value;
// key是ThreadLocal实例,用弱引用包装
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
这里有个关键:Entry
的 key(ThreadLocal 实例)是弱引用 (WeakReference
)。这是为了避免内存泄漏:当 ThreadLocal 实例不再被使用时(比如被回收),即使线程还在运行,这个 key 也会被自动清理。
总结:ThreadLocal 的实现逻辑
- 每个线程(Thread)都有一个专属的 "储物柜列表"(ThreadLocalMap)
- ThreadLocal 实例相当于 "钥匙",用来在列表中定位对应的 "柜子"(Entry)
- 调用
set(value)
时:用当前 ThreadLocal 当钥匙,把值存入当前线程的列表中 - 调用
get()
时:用当前 ThreadLocal 当钥匙,从当前线程的列表中取对应的值 - 因为每个线程只能访问自己的列表,所以变量自然就线程隔离了
额外注意:内存泄漏问题
虽然Entry
的 key 是弱引用,但 value 是强引用。如果线程长期运行(比如线程池里的核心线程),且忘了调用remove()
,那么即使 key 被回收,value 也会一直占着内存,造成泄漏。
所以使用 ThreadLocal 后,最好在合适的时机(比如线程执行结束前)调用remove()
清理:
java
userThreadLocal.remove(); // 清空当前线程中该ThreadLocal对应的值
用一句话总结:ThreadLocal 通过让每个线程维护自己的变量副本,实现了线程间的变量隔离,而这个 "维护" 的核心就是线程内部的 ThreadLocalMap 和 ThreadLocal 作为钥匙的设计。