前言
在日常的工作中,你有用过ThreadLocal
或者InheritableThreadLocal
吗? 相信有很多经常写的朋友,也有很多没写过这玩意的朋友,但面试偶尔还是会碰到这种情况的。 那ThreadLocal
是啥?InheritableThreadLocal
又是啥?
简单来说,InheritableThreadLocal 是 ThreadLocal 的升级版。它解决了 ThreadLocal 的一个痛点:父线程创建的变量无法传递给子线程。
别小看它。平时不显山不露水。一旦你在做用户上下文传递、链路追踪、日志埋点这些类似功能时,它就变得很关键。
尤其是你用了多线程、线程池、异步任务......
搞不好,上下文就丢了。
感兴趣的朋友可以往下继续了解。
一、ThreadLocal 是啥?
简单说:
ThreadLocal
是一个能让每个线程拥有自己独立变量副本的工具。
什么意思?比如你有一个全局变量:
java
private static String user = "张三";
问题来了:
多线程下,大家都能改这个user
。
线程A改成"李四",线程B又改成"王五"。
直接乱套了!
怎么解决?这时候就要用到ThreadLocal
了:
java
private static ThreadLocal<String> userHolder = new ThreadLocal<>();
现在:
- 线程A 设置:
userHolder.set("张三")
→ 它拿到的就是"张三" - 线程B 设置:
userHolder.set("李四")
→ 它拿到的就是"李四"
互不干扰,就像每人发了个独立的小本本,写啥都只自己看得见。
二、为什么要用 ThreadLocal?
常见用途有这几个:
1. 用户上下文保存
用户登录后,把当前用户的userId
、userName
等身份信息存进ThreadLocal
。这样在后续的Controller、Service、DAO层任何地方,都能直接获取用户信息,不需要在每个方法参数里传来传去。
java
// 登录时存储
User user = getUserFromToken(token);
UserContext.setCurrentUser(user);
// 业务方法中直接使用
public void doBusiness() {
User currentUser = UserContext.getCurrentUser(); // 无需参数传递
System.out.println("当前用户: " + currentUser.getName());
}
2. 数据库事务管理
Spring 的事务管理就是基于ThreadLocal
实现的。它把数据库连接绑定到当前线程,确保同一个事务中的所有数据库操作都用同一个连接。
java
// Spring事务管理的简化原理
public class TransactionManager {
private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<>();
public static Connection getConnection() {
Connection conn = connectionHolder.get();
if (conn == null) {
conn = dataSource.getConnection();
connectionHolder.set(conn);
}
return conn;
}
}
3. 代码简化
避免了在方法调用链中层层传递公共参数(traceId、userId、tenantId等),让代码更简洁清晰。
java
// 没有ThreadLocal时:参数需要层层传递
controller.method(traceId, userId);
service.method(traceId, userId);
dao.method(traceId, userId);
// 有ThreadLocal时:参数只需设置一次
TraceContext.setTraceId(traceId);
UserContext.setUserId(userId);
controller.method();
service.method(); // 直接从这里获取
dao.method(); // 直接从这里获取
4. 链路追踪
在微服务架构中,ThreadLocal
用于传递traceId
,实现请求链路的追踪。
java
// 在过滤器或拦截器中
String traceId = generateTraceId();
TraceContext.setTraceId(traceId);
// 后续所有日志都能带上这个traceId
log.info("[{}] 开始处理请求", TraceContext.getTraceId());
三、ThreadLocal 是怎么实现的?
ThreadLocal
的实现很巧妙,它在线程对象内部维护了一个类似HashMap
的结构:
java
// Thread类中有这两个重要属性
ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
// ThreadLocal的set方法源码
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = t.threadLocals;
if (map != null) {
map.set(this, value);
} else {
createMap(t, value);
}
}
当你调用:
java
threadLocal.set("abc");
JVM 会:
- 找到当前线程(
Thread.currentThread()
) - 拿到它的
threadLocals
map - 把
threadLocal
当 key,"abc"
当 value,存进去
取值时也一样:
java
threadLocal.get();
找当前线程 → 找它的map
→ 拿threadLocal
对应的值。
所以,每个线程的数据是隔离的。 A线程存的,B线程看不到。
四、ThreadLocal 可能出现哪些问题?
问题1:内存泄漏!
案例
java
public class MemoryLeakExample {
private static ThreadLocal<byte[]> localVariable = new ThreadLocal<>();
public static void main(String[] args) {
new Thread(() -> {
localVariable.set(new byte[1024 * 1024 * 10]); // 10MB
// 使用完后没有remove
// 线程结束后,value仍然被ThreadLocalMap引用,无法被GC回收
}).start();
}
}
原因: ThreadLocalMap
的key是弱引用,但value是强引用。如果ThreadLocal
实例被回收,key变成null,但value仍然存在,导致内存泄漏。
所以每次用完,记得调 remove()
!
java
try {
threadLocal.set("xxx");
// 业务逻辑
} finally {
threadLocal.remove(); // 必须!
}
问题2:子线程拿不到父线程的值!
案例:
java
public class ThreadLocalCase {
private static ThreadLocal<String> tl = new ThreadLocal<>();
public static void main(String[] args) {
tl.set("主线程的值");
new Thread(() -> {
System.out.println("子线程:" + tl.get());
}).start();
System.out.println("主线程:" + tl.get());
}
}
输出:
csharp
主线程:主线程的值
子线程:null
啥?子线程拿不到?! 对!因为ThreadLocal
只在当前线程有效。子线程创建时,它的threadLocals
是空的,啥都没继承,这就麻烦了。
比如你主线程存了用户ID。子线程要记录日志、发消息。
结果拿不到用户ID,日志都打不出来。
那怎么办?这时候就要用InheritableThreadLocal
了。
使用的时候改一下代码:
java
private static InheritableThreadLocal<String> itl = new InheritableThreadLocal<>();
其他不变。输出变成:
主线程:主线程的值
子线程:主线程的值
五、InheritableThreadLocal 是怎么做到的?
核心源码分析
InheritableThreadLocal
不是凭空来的。它是继承的ThreadLocal
。
java
public class InheritableThreadLocal<T> extends ThreadLocal<T> {
// ...
}
但它做了三件关键的事:
- 重写了
childValue()
方法 - 重写了
getMap()
方法 - 重写了
createMap()
方法
我们一个一个看。
1. 关键方法一:childValue?
java
protected T childValue(T parentValue) {
return parentValue;
}
这是最核心的方法!它决定:子线程拿到的值,是怎么算出来的。默认就是直接返回父线程的值。也就是原样继承。
但你可以重写它!比如你想让子线程的值加个前缀:
java
InheritableThreadLocal<String> itl = new InheritableThreadLocal<String>() {
@Override
protected String childValue(String parentValue) {
return "[子线程]" + parentValue;
}
};
这样,子线程拿到的就是 "子线程]主线程值"
。
2. 关键方法二和三:getMap 和 createMap
ThreadLocal
和InheritableThreadLocal
用的不是同一个存储区。
ThreadLocal
用的是:threadLocals
InheritableThreadLocal
用的是:inheritableThreadLocals
怎么做到的?靠这两个方法:
java
// 返回 inheritableThreadLocals,不是 threadLocals
ThreadLocalMap getMap(Thread t) {
return t.inheritableThreadLocals;
}
// 创建 inheritableThreadLocals
void createMap(Thread t, T firstValue) {
t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
}
InheritableThreadLocal
把数据存在另一个抽屉里。这个抽屉专门用来传给子线程。
3. Thread 初始化时的拷贝
前面都是准备。真正的继承,发生在子线程创建时。
我们看Thread
的init()
方法(简化版):
java
private void init(...) {
Thread parent = currentThread(); // 找到父线程
// 如果父线程有 inheritableThreadLocals
if (inheritThreadLocals && parent.inheritableThreadLocals != null) {
// 就创建一个继承的 map
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
}
}
子线程一创建,JVM 就检查:"父线程有没有可继承的数据?"。 如果有,就调用createInheritedMap
,把数据拷贝过来。
4. createInheritedMap:怎么拷贝的?
这个方法在 ThreadLocal
类里:
java
static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
return new ThreadLocalMap(parentMap);
}
它调用了 ThreadLocalMap
的一个特殊构造函数:
java
private ThreadLocalMap(ThreadLocalMap parentMap) {
Entry[] parentTable = parentMap.table;
int len = parentTable.length;
setThreshold(len);
table = new Entry[len]; // 新建一个 table
for (int j = 0; j < len; j++) {
Entry e = parentTable[j];
if (e != null) {
ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
if (key != null) {
// 调用 childValue 计算子线程的值
Object value = key.childValue(e.value);
Entry c = new Entry(key, value);
int h = key.threadLocalHashCode & (len - 1);
while (table[h] != null)
h = nextIndex(h, len);
table[h] = c;
size++;
}
}
}
}
关键点:
- 遍历父线程的
table
- 拿到每个
Entry
- 调用
key.childValue(e.value)
计算子线程的值 - 放到子线程的新
table
中
六. InheritableThreadLocal 在线程池中的问题
来看这个例子:
java
ExecutorService executor = Executors.newFixedThreadPool(1);
InheritableThreadLocal<String> context = new InheritableThreadLocal<>();
// 第一次提交任务
context.set("task1");
executor.submit(() -> {
System.out.println(context.get()); // 输出: task1
});
// 第二次提交任务
context.set("task2");
executor.submit(() -> {
System.out.println(context.get()); // 输出啥?
});
你以为输出 task2
?但也有可能还是 task1
!为什么?
因为线程池里的线程是复用的 。第一次任务执行完,线程没销毁。inheritableThreadLocals
里的值还在。
第二次任务来了,线程还是那个线程。InheritableThreadLocal
的值没变。所以输出的还是 task1
。
总结
ThreadLocal
:线程私有变量,隔离数据。ThreadLocal
有内存泄漏风险,记得remove()
。ThreadLocal
不支持子线程继承。InheritableThreadLocal
:解决继承问题,子线程创建时自动拷贝。但它在线程池中会污染上下文。- 线程池推荐使用
TransmittableThreadLocal
(TTL)。
InheritableThreadLocal
很有用。但只适合"每次创建新线程"的场景。
如果你用线程池,这里推荐另外一个,那就是TransmittableThreadLocal(TTL)。用法几乎一样,它能解决线程复用下的传递问题。
公众号:程序员刘大华,专注分享前后端开发的实战笔记。关注我,少走弯路,一起进步!
📌往期精彩
《工作 5 年没碰过分布式锁,是我太菜还是公司太稳?网友:太真实了!》
《90%的人不知道!Spring官方早已不推荐@Autowired?这3种注入方式你用对了吗?》