异步线程变量传递必知必会---InheritableThreadLocal及底层原理分析

InheritableThreadLocal简介

上一篇文章我们聊到了ThreadLocal的作用机理,但是在文章的末尾,我提到了一个问题,ThreadLocal无法实现异步线程变量的传递。

什么意思呢?以下面的代码为例子:

@SneakyThrows
public Boolean testThreadLocal(String s){
    LOGGER.info("实际传入的值为: " + s);
    DemoContext.setContext(Integer.valueOf(s)); // DemoContext为相应的ThreadLocal对象
    CompletableFuture<Throwable> subThread = CompletableFuture.supplyAsync(()->{
        try{
            //打印子线程的值
            LOGGER.info(String.format("子线程id=%s,contextStr为:%s",
                                      Thread.currentThread().getId(),DemoContext.getContext()));
        }catch (Throwable throwable){
            return throwable;
        }
        return null;
    });
    //打印主线程的值
    LOGGER.info(String.format("主线程id=%s,contextStr为:%s",
                              Thread.currentThread().getId(),DemoContext.getContext()));
    Throwable throwable = subThread.get();
    if (throwable!=null){
        throw throwable;
    }
    DemoContext.clearContext();
    return true;
}

原本我们期待的结果是,子线程中的值与主线程中的值保持一致,但是实际上,运行代码返回的结果是:

由此可见,ThreadLocal并没有按照所想的那样将相应的ThreadLocal的值传递到相应的异步线程上。

为了实现异步线程变量的传递,InheritableThreadLocal应运而生(以下简称为:ITL)。

我们将上述的代码稍作改动,将demoContext的类型转换成ITL之后再运行一次代码。可以看到结果如下:

 

ITL虽然传递了主线程的变量信息,但是在特定场景下也会出现问题。例如在上面的代码中,如果我们设置相应的线程池再来请求的话,就会出现问题。源码如下:

@SneakyThrows
public Boolean testThreadLocal(String s){
    ...

    CompletableFuture<Throwable> subThread = CompletableFuture.supplyAsync(()->{
        try{
            //打印子线程的值
            LOGGER.info(String.format("子线程id=%s,contextStr为:%s",
                                      Thread.currentThread().getId(),DemoContext.getContext()));
        }catch (Throwable throwable){
            return throwable;
        }
        return null;
    },demoExecutor); // 设置了线程池

    ...
}

@Bean(name = "demoExecutor")
public Executor demoExecutor() {
    ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor();
    threadPoolTaskExecutor.setTaskDecorator(new GatewayHeaderTaskDecorator());
    threadPoolTaskExecutor.setCorePoolSize(5);
    threadPoolTaskExecutor.setQueueCapacity(0);
    threadPoolTaskExecutor.setKeepAliveSeconds(3600);
    threadPoolTaskExecutor.setMaxPoolSize(1);
    threadPoolTaskExecutor.setThreadNamePrefix("demoExecutor-");
    threadPoolTaskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
    threadPoolTaskExecutor.setWaitForTasksToCompleteOnShutdown(true);
    threadPoolTaskExecutor.initialize();
    return threadPoolTaskExecutor;
}

代码运行起来的结果如下:

可以看到,多次请求后,线程间的变量出现了混乱传递,即实际传入的值,与子线程中拿到的值并不一样。这又是什么原因呢?

底层原理分析

要了解这个问题的原因,我们不得不了解下ITL的工作机制。

其实看起来,但是其实ITL的工作机制很简单,就是在子线程初始化的时候,将父线程的ITL给继承过来。具体来看Thread类中相应的init源码:

private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc,
                      boolean inheritThreadLocals) {
        Thread parent = currentThread(); // 先找到他的爸爸

        this.daemon = parent.isDaemon(); //判断是否需要创建的是daemon线程
        this.priority = parent.getPriority(); // 保持跟他爸一样的优先级
        if (security == null || isCCLOverridden(parent.getClass())) // 获取相应的加载器
            this.contextClassLoader = parent.getContextClassLoader();
        else
            this.contextClassLoader = parent.contextClassLoader;
        this.inheritedAccessControlContext =
                acc != null ? acc : AccessController.getContext();
        this.target = target;
        setPriority(priority);
        /*关键性代码*/
        // 在这里会判断当前的是否需要inheritThreadLocals
        //如果需要,那么会将当前创建这个线程的InheritableThreadLocals都获取过来,相当于获取了一份父类表的拷贝。
        if (inheritThreadLocals && parent.inheritableThreadLocals != null)
            this.inheritableThreadLocals =
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
        this.stackSize = stackSize;
        tid = nextThreadID(); // 设置ThreadID
    }

这种场景,在不使用线程池的情况是没有问题的。但是如果搭配上了线程池,就会存在问题。这里我们先简单介绍一下线程池的作用机理。

其中最关键的点在于,线程池会复用原有线程,致使部分线程不会经过Init初始化的过程,ITL的值也就没有办法得到更新。最终造成了错误的数据传递。

优劣势分析

优势:

1、通过在线程初始化的时候传递相应的ThreadLocal变量,解决了非线程池下的异步线程的变量传递问题。

劣势:

1、线程池复用线程和ITL底层机制无法兼容,导致了ITL无法结合线程池发挥作用。

总结:

在不依赖于线程池的场景下,ITL是一个很好的实现异步线程传递变量的工具。

然而,在使用线程池的情况下,由于线程不会进行频繁地初始化和销毁等工作,ITL的变量值无法得到更新,因而有可能存在数据错误传递的问题。

转自:https://zhuanlan.zhihu.com/p/469859090

posted @ 2023-08-11 14:01  甜菜波波  阅读(177)  评论(0编辑  收藏  举报