💬前言
我是[提前退休的java猿],一名7年java开发经验的开发组长,分享工作中的各种问题!
最近遇到一个问题,在异步响应的请求中遇到 获取不到shiro
上下文资源的情况,这个问题就引出为什么有些异步代码中又能获取到shiro
的上下文资源呢?
这就引出了shiro
的上下文资源是如何传递的?没错就是通过InheritableThreadLocal
包装来传递的。
今天就来说一下这个属性,其中也是有很多坑儿需要注意的🤪
📃认识 InheritableThreadLocal
首先ThreadLocal
大家都比较熟悉,能够把变量绑定到线程上,各线程独立维护互不影响(引用类型copy引用地址)。InheritableThreadLocal
主要是解决父子线程之间的数据传递问题。
➡使用示例
java
public class InheritableThreadLocalDemo {
// 创建可继承的线程本地变量,用于存储用户上下文
private static final ThreadLocal<UserContext> userContext = new InheritableThreadLocal<>();
// 模拟用户上下文类
static class UserContext {
private String userId;
private String userName;
public UserContext(String userId, String userName) {
this.userId = userId;
this.userName = userName;
}
@Override
public String toString() {
return "UserContext{userId='" + userId + "', userName='" + userName + "'}";
}
}
public static void main(String[] args) {
// 父线程设置上下文
userContext.set(new UserContext("u123", "张三"));
System.out.println("父线程上下文: " + userContext.get());
// 创建子线程1 - 直接使用继承的上下文
Thread childThread1 = new Thread(() -> {
System.out.println("子线程1获取到的上下文: " + userContext.get());
}, "子线程1");
childThread1.start();
// 清理资源
userContext.remove();
}
}
运行输出如下:
js
父线程上下文: UserContext{userId='u123', userName='张三'}
子线程1获取到的上下文: UserContext{userId='u123', userName='张三'}
➡实现原理
继承机制的核心,当创建新线程时,JVM会调用Thread构造方法时候,会复制父线程的inheritableThreadLocals,伪代码如下:
java
public class Thread implements Runnable {
........
/*
* 存储父线程的localMap
* InheritableThreadLocal values pertaining to this thread. This map is
* maintained by the InheritableThreadLocal class.
*/
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
........
// Thread类的构造方法,inheritThreadLocals 默认传的true
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,boolean inheritThreadLocals) {
// ...
// 复制父线程的inheritableThreadLocals
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
.........
}
}
复制逻辑如下:
- 当父线程创建子线程时
- JVM检查父线程的
inheritableThreadLocals
是否为空 - 如果不为空,则创建子线程的
inheritableThreadLocals
映射 - 遍历父线程的所有InheritableThreadLocal变量
- 调用
childValue()
方法获取值并存入子线程的map
💫避坑 InheritableThreadLocal
➡避免在线程池中使用InheritableThreadLocal包装的信息
线程池中的线程被重用时,残留的上下文会污染后续任务。 因为InheritableThreadLocal
是在创建的时候被copy到子线程中的,所以线程池中线程的InheritableThreadLocal
默认会复用第一次创建的。
代码示例如下:
java
static Executor executor = Executors.newSingleThreadExecutor();
public static void main(String[] args) {
//1. 发起请求1:假设现在是 用户 张三 发起请求
userContext.set(new UserContext("u123", "张三"));
executor.execute(()->{
System.out.println("子线程获取到的上下文: " + userContext.get());
});
// 模拟请求1执行完成
Thread.sleep(1000);
// 2. 发起请求2:假设现在是 用户 李四 发起请求
userContext.set(new UserContext("123", "李四"));
System.out.println("父线程上下文: " + userContext.get());
executor.execute(()->{
System.out.println("子线程获取到的上下文: " + userContext.get());
});
// 清理资源
userContext.remove();
}
输出如下:
js
子线程获取到的上下文: UserContext{userId='u123', userName='张三'}
父线程上下文: UserContext{userId='123', userName='李四'}
子线程获取到的上下文: UserContext{userId='u123', userName='张三'}
📖所以如果我们在业务代码在线程池中获取过
InheritableThreadLocal
变量,比如shiro的用户信息,那么就会出现幽灵代码。明明是A用户,获取到信息可能是B用户。
✔解决方案
尽量不要异步方法中获取 InheritableThreadLocal 资源,如果非要在线程池中获取,那么就在当前线程中获取上InheritableThreadLocal 资源,传递到线程中然后替换。或者使用包装后的线程池,原理也是在异步执行前后重置InheritableThreadLocal 资源
Spring 的 ThreadPoolTaskExecutor
提供了 TaskDecorator
接口,可在任务执行前后拦截并处理上下文,是线程池环境下最优雅的解决方案。
✅推荐大家直接使用:
TransmittableThreadLocal
(简称 TTL)是阿里巴巴开源的一个 Java 工具类。下面的示例只是不单独引入依赖,以及方便了解 处理的原理。
🔈示例代码:
java
@Configuration
public class TraceIdThreadPoolConfig {
// 模拟存储TraceID的InheritableThreadLocal
public static final InheritableThreadLocal<String> TRACE_ID = new InheritableThreadLocal<>();
@Bean(name = "traceIdExecutor")
public Executor traceIdExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(1); // 核心线程数
executor.setMaxPoolSize(1); // 最大线程数
executor.setQueueCapacity(10); // 队列容量
executor.setThreadNamePrefix("trace-"); // 线程名前缀
// 设置TaskDecorator处理上下文
executor.setTaskDecorator(runnable -> {
// 1. 捕获提交任务的线程(父线程)的上下文
String parentTraceId = TRACE_ID.get();
// 2. 包装任务,执行前后处理上下文
return () -> {
String originalTraceId = null;
try {
// 执行前:将父线程的上下文设置到工作线程
originalTraceId = TRACE_ID.get(); // 保存工作线程原有值(可能为null)
TRACE_ID.set(parentTraceId);
// 执行实际任务
runnable.run();
} finally {
// 执行后:恢复工作线程原有值(清理上下文,避免污染)
if (originalTraceId == null) {
TRACE_ID.remove(); // 若原有值为null,直接移除
} else {
TRACE_ID.set(originalTraceId); // 否则恢复原有值
}
}
};
});
// 拒绝策略:当线程和队列都满时,由提交任务的线程执行
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
return executor;
}
}
➡引用类型共享
如果InheritableThreadLocal存储可变对象,父子线程会共享同一实例
🔈示例:
java
// 创建InheritableThreadLocal存储可变对象
private static final InheritableThreadLocal<List<String>> threadLocal =
new InheritableThreadLocal<List<String>>() {
@Override
protected List<String> initialValue() {
return new ArrayList<>();
}
};
public static void main(String[] args) {
// 父线程添加数据
threadLocal.get().add("父线程添加的数据");
// 创建子线程
Thread childThread = new Thread(() -> {
// 子线程获取并修改数据
List<String> list = threadLocal.get();
list.add("子线程添加的数据");
});
}
✔解决方案
重写childValue方法就行了:
java
// 创建InheritableThreadLocal存储可变对象
private static final InheritableThreadLocal<List<String>> threadLocal =
new InheritableThreadLocal<List<String>>() {
@Override
protected List<String> initialValue() {
return new ArrayList<>();
}
@Override
protected List<String> childValue(List<String> parentValue) {
// 创建新的列表并复制元素,实现深拷贝
return new ArrayList<>(parentValue);
}
};
📖总结
InheritableThreadLocal 在开发中经常遇到,只是被框架封装了,导致我们开发都忽略了这东西。在异步编程中可能发现,线程池也能通过工具类获取用户信息,测试也没测试问题,但是到了生产环境就可能出现幽灵事件。
InheritableThreadLocal解决了父子线程间数据传递的难题,同时呢也存在一些坑儿,在使用过程中下面几点我们就要掌握呢:
- 深入理解继承机制 :明白值是如何从父线程传递到子线程的(
Thread
构造方法) - 严格生命周期管理:确保及时清理避免内存泄漏
- 防御性编程 :对可变对象进行深拷贝(重写
childValue
) - 线程池特殊处理 :使用装饰器或专门库处理线程复用(
TransmittableThreadLocal
(简称 TTL))
当您掌握了这些技巧,InheritableThreadLocal将成为处理异步任务上下文传递的强大工具,特别是在安全认证、链路追踪等场景中发挥关键作用。