什么是inheritableThreadLocal
顾名思义,他是一个可继承的ThreadLocal,它是Threadocal 的一个扩展,用于在线程创建时将父线程的 ThreadLocal 变量副本传递给子线程,使得子线程可以访问父线程中设置的本地变量。它解决了 Threadloca 无法在子线程中继承父线程本地变量的问题。
工作原理:
当创建子线程时, InheritableThreadLocal 的值会被自动拷贝到子线程中。
子线程可以修改自己的副本,但不会影响父线程的值。
源码分析
在Thread中其实是有这个成员的

在父线程创建子线程的时候,子线程可以使用构造函数获取父线程,然后判断父线程下的InheritableThreadLocal是否有值,如果有就拷贝。


使用示例
public class InheritableThreadLocalExample {
// 创建一个 InheritableThreadLocal 用来存储上下文信息
private static final InheritableThreadLocal<String> context = new InheritableThreadLocal<>();
public static void main(String[] args) {
// 设置父线程中的变量
context.set("Parent Thread Data");
// 创建子线程
Thread childThread = new Thread(() -> {
System.out.println("Child Thread Initial Value: " + context.get());
// 子线程中修改值
context.set("Child Thread Data");
System.out.println("Child Thread Modified Value: " + context.get());
});
// 启动子线程
childThread.start();
// 主线程输出
System.out.println("Parent Thread Value: " + context.get());
}
}
货拉拉出现故障的原因
原文链接https://juejin.cn/post/7537593417100115977
业务场景
有一个系统需要执行大量数据的重算,我们会依据指定条件从数据库中提取入参数据,并使用这些数据进行业务计算。
由于数据量巨大,每次都会有将几千万级别的数据,单台机器执行耗时较长,不能满足业务的需求。为了提升执行的速度,我们引入xxljob,将庞大数据量分摊到多台机器执行来缩减总体执行时间。
整体任务的执行逻辑如下:
- 通过 xxl-job 向多台机器分发任务,携带总的数据 ID 区间。
- 每台机器收到任务后,使用统一算法对总区间进行划分,仅处理其负责的子区间。
例如:
| 机器编号 | 数据区间 |
| 机器 A | 0 ~ 20w |
| 机器 B | 20w ~ 40w |
| ... | ... |
出现的问题
- 实际执行完成的数据量少于预期
- 刚刚发布的几天内不会失败,执行一段时间后会失败,重启后能执行成功,再执行一段时间后还是会失败
排查思路
- 基础设施/部署环境排查
CPU 负载、内存是否过高 ,IO读写是否报错,网络是否抖动等。
- 系统层排查
缓存是否污染,线程池是否阻塞,JVM相关指标是否正常
- 调度层排查
xxljob轮询是否均衡,是否有调度失败的情况,机器是否频繁上下线或心跳丢失,调度参数是否有误,
xxl-job 日志是否有异常等
- 应用层排查
日志是否有异常,错误链路追踪等
问题分析
最终使用加入大量的日志排查出了问题

业务代码
@HllXxlJob(value = "DivideTaskJob")
public void DivideTaskJob() {
log.info("originParam:{}", HllXxlJobManager.getJobParam());
handleExecutor.execute(() -> {
try {
String command = HllXxlJobManager.getJobParam() ;
log.info("divideParam:{}", HllXxlJobManager.getJobParam());
......
ParamDTO param = JSONUtils.parseJson(command, ParamDTO.class);
Long taskId = param.getTaskId();
Long minId = param.getMinId();
Long maxId = param.getMaxId();
......
} catch (Exception e) {
log.error("IndependentDemandJobHandler.executeIndependentDemandDivideTaskJob error", e);
}
});
}
根据日志可以看出,部分机器(如图pod1和pod8)执行过程中,第3行主线程获取的参数是
{"taskId":4111, "minId":20000001, "maxId":"41000000"},
但是第7行线程池获取的参数却是
{"taskId":4110, "minId":0, "maxId":"20000000"}
说明这个异常很有可能和HllXxlJobManager.getJobParam()有关所以需要观察一下xxljob提供的HllXxlJobManager.getJobParam()的源码了
HllXxlJobManager源码分析
参数写入逻辑是这样的

这里可以看出参数被写入了一个InheritableThreadLocal。看到这里基本上其实已经有结论了
产生问题的原因
在InheritableThreadLocal中是支持父子线程之间的传递的,这个传递动作发生在新建子线程的时候创建的,在生成环境中一般都会使用线程池来压榨机器效率的,在一开始数据量并不大,所以每个任务会创建一个新的线程,不会复用之前创建的线程。但随着核心任务的增多,新任务来到线程池,不会再创建新的线程,而是将任务进行排队服用之前的线程。但是之前线程池中的上下文参数是没有清除的。在此时调用不会创建新线程,也不会重置参数。
这也解释了为什么重启之后线程池可以OK一阵子,但过一段时间又不行的原因了
解决方案(个人猜想)
在这种业务场景下就没办法直接使用InheritableThreadLocal了,应该做的是避免线程复用带来的旧上下文干扰。
解决方案一:手动清理 InheritableThreadLocal 上下文(轻量通用)
核心逻辑:
线程池复用线程时,旧任务的 InheritableThreadLocal 值会残留在线程中。可在任务执行前清除旧值、执行后恢复或彻底清理,确保新任务不受旧上下文影响。
实现步骤:
- 执行前预处理:任务开始时,检查当前线程的 InheritableThreadLocal 是否有旧值,若有则先保存(可选)再移除;
- 执行任务逻辑:正常执行业务代码;
- 执行后清理:任务结束后,若之前保存了旧值则恢复(避免影响其他任务),或直接彻底清除(若线程仅执行同类任务)。
public class CleanableTask implements Runnable {
// 业务参数(替代InheritableThreadLocal传递)
private final JobParam jobParam;
// 保存旧的InheritableThreadLocal值(用于恢复)
private String oldContextValue;
public CleanableTask(JobParam jobParam) {
this.jobParam = jobParam;
}
@Override
public void run() {
try {
// 1. 执行前:清除旧的InheritableThreadLocal上下文
oldContextValue = HllXxlJobManager.getContext().get(); // 假设getContext()返回InheritableThreadLocal实例
if (oldContextValue != null) {
HllXxlJobManager.getContext().remove(); // 移除旧值
}
// 2. 执行业务逻辑(使用显式传递的jobParam,而非从InheritableThreadLocal获取)
System.out.println("执行任务,参数:" + jobParam);
processData(jobParam);
} finally {
// 3. 执行后:恢复旧值(若线程需复用给其他任务,避免上下文污染)
if (oldContextValue != null) {
HllXxlJobManager.getContext().set(oldContextValue);
} else {
// 若无需恢复,直接清除(防止空值残留)
HllXxlJobManager.getContext().remove();
}
}
}
private void processData(JobParam jobParam) {
// 业务处理逻辑
}
}
// 线程池使用方式
ExecutorService executor = Executors.newFixedThreadPool(5);
JobParam param = new JobParam(4111, 20000001, 41000000);
executor.submit(new CleanableTask(param));
解决方案二:使用线程池装饰器(统一管控)
核心逻辑:
通过装饰器模式包装 ThreadPoolExecutor,在任务提交到线程池时,自动注入 “上下文清理 - 恢复” 逻辑,无需每个任务单独写清理代码,实现统一管控。
实现步骤:
- 自定义一个
ThreadPoolExecutor,继承ThreadPoolExecutor; - 重写
execute()或submit()方法,在提交任务时,用Runnable/Callable包装类包裹原任务,注入清理逻辑; - 所有任务通过装饰后的线程池执行,自动完成上下文处理。
// 1. 自定义任务包装类:注入上下文清理逻辑
class ContextCleaningRunnable implements Runnable {
private final Runnable target;
private String oldContextValue;
public ContextCleaningRunnable(Runnable target) {
this.target = target;
}
@Override
public void run() {
try {
// 执行前清除旧上下文
oldContextValue = HllXxlJobManager.getContext().get();
if (oldContextValue != null) {
HllXxlJobManager.getContext().remove();
}
// 执行原任务
target.run();
} finally {
// 执行后恢复旧上下文
if (oldContextValue != null) {
HllXxlJobManager.getContext().set(oldContextValue);
} else {
HllXxlJobManager.getContext().remove();
}
}
}
}
// 2. 装饰器线程池
class DecoratedThreadPoolExecutor extends ThreadPoolExecutor {
public DecoratedThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
}
@Override
public void execute(Runnable command) {
// 提交任务时,用包装类包裹原任务
super.execute(new ContextCleaningRunnable(command));
}
@Override
public <T> Future<T> submit(Callable<T> task) {
// 对Callable任务同样包装(需额外实现ContextCleaningCallable)
return super.submit(new ContextCleaningCallable<>(task));
}
}
// 3. 使用装饰后的线程池
ExecutorService executor = new DecoratedThreadPoolExecutor(5, 10, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<>());
// 提交任务时无需手动处理上下文
executor.execute(() -> {
JobParam param = new JobParam(4111, 20000001, 41000000);
processData(param);
});
解决方案三:使用 TransmittableThreadLocal(TTL)(第三方增强方案)
核心逻辑:
InheritableThreadLocal的核心缺陷是不支持线程池复用场景的上下文传递,而阿里开源的TransmittableThreadLocal(TTL)专门解决此问题:
- 自动在 “线程池任务提交时复制上下文”,“任务执行后恢复原线程上下文”;
- 支持线程池、线程复用场景,无需手动清理,兼容性强。
实现步骤:
- 引入依赖:在 Maven/Gradle 中添加 TTL 依赖;
- 替换 InheritableThreadLocal:将原代码中
InheritableThreadLocal替换为com.alibaba.ttl.TransmittableThreadLocal; - 包装线程池:使用
TTL提供的TtlExecutors装饰线程池,自动启用上下文传递。
解决方案四:显式传递任务参数
核心逻辑:
完全抛弃InheritableThreadLocal,将任务所需的上下文(如 taskId、数据区间)作为任务的显式参数,通过Runnable/Callable的构造函数传入,从根本上避免上下文污染问题。
// 1. 定义业务参数类
class JobParam {
private int taskId;
private long minId;
private long maxId;
// 构造函数、getter
public JobParam(int taskId, long minId, long maxId) {
this.taskId = taskId;
this.minId = minId;
this.maxId = maxId;
}
}
// 2. 任务类:通过构造函数接收参数(无InheritableThreadLocal)
class BusinessTask implements Runnable {
private final JobParam jobParam;
// 显式传入参数
public BusinessTask(JobParam jobParam) {
this.jobParam = jobParam;
}
@Override
public void run() {
// 直接使用显式传递的参数,无需从ThreadLocal获取
System.out.println("执行任务:taskId=" + jobParam.getTaskId() + ", 区间=" + jobParam.getMinId() + "-" + jobParam.getMaxId());
processData(jobParam);
}
private void processData(JobParam param) {
// 业务逻辑
}
}
// 3. 线程池使用
ExecutorService executor = Executors.newFixedThreadPool(5);
// 提交任务时传入具体参数
JobParam param1 = new JobParam(4111, 20000001, 41000000);
executor.submit(new BusinessTask(param1));
JobParam param2 = new JobParam(4112, 41000001, 60000000);
executor.submit(new BusinessTask(param2));

Comments NOTHING