inheritableThreadLocal的原理以及为什么货拉拉使用这个技术会出现问题

xiaojiuaigc@163.com 发布于 2025-08-31 279 次阅读


什么是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 区间。
  • 每台机器收到任务后,使用统一算法对总区间进行划分,仅处理其负责的子区间。

例如:

机器编号数据区间
机器 A0 ~ 20w
机器 B20w ~ 40w
......

出现的问题

  • 实际执行完成的数据量少于预期
  • 刚刚发布的几天内不会失败,执行一段时间后会失败,重启后能执行成功,再执行一段时间后还是会失败

排查思路

  1. 基础设施/部署环境排查

CPU 负载、内存是否过高 ,IO读写是否报错,网络是否抖动等。

  1. 系统层排查

缓存是否污染,线程池是否阻塞,JVM相关指标是否正常

  1. 调度层排查

xxljob轮询是否均衡,是否有调度失败的情况,机器是否频繁上下线或心跳丢失,调度参数是否有误,

xxl-job 日志是否有异常等

  1. 应用层排查

日志是否有异常,错误链路追踪等

问题分析

最终使用加入大量的日志排查出了问题

业务代码

@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 值会残留在线程中。可在任务执行前清除旧值、执行后恢复或彻底清理,确保新任务不受旧上下文影响。

实现步骤:

  1. 执行前预处理:任务开始时,检查当前线程的 InheritableThreadLocal 是否有旧值,若有则先保存(可选)再移除;
  2. 执行任务逻辑:正常执行业务代码;
  3. 执行后清理:任务结束后,若之前保存了旧值则恢复(避免影响其他任务),或直接彻底清除(若线程仅执行同类任务)。
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,在任务提交到线程池时,自动注入 “上下文清理 - 恢复” 逻辑,无需每个任务单独写清理代码,实现统一管控。

实现步骤:

  1. 自定义一个ThreadPoolExecutor,继承ThreadPoolExecutor
  2. 重写execute()submit()方法,在提交任务时,用Runnable/Callable包装类包裹原任务,注入清理逻辑;
  3. 所有任务通过装饰后的线程池执行,自动完成上下文处理。
// 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)专门解决此问题:

  • 自动在 “线程池任务提交时复制上下文”,“任务执行后恢复原线程上下文”;
  • 支持线程池、线程复用场景,无需手动清理,兼容性强。

实现步骤:

  1. 引入依赖:在 Maven/Gradle 中添加 TTL 依赖;
  2. 替换 InheritableThreadLocal:将原代码中InheritableThreadLocal替换为com.alibaba.ttl.TransmittableThreadLocal
  3. 包装线程池:使用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));