父子线程变量传递,价值两个p0的代码修复

问题复现:

项目内原本采用的是DemoContext作为一个线程的上下文context,用于存储从header头、入参数的一部分数据,实现跨业务代码复用及传递。

public class DemoContext {
    ...
  
    //创建一个ThreadLocal
  private static final ThreadLocal CONTEXT_HOLDER = new ThreadLocal<>();
        
  ...
}
    @SneakyThrows
    public Boolean testThreadLocal(String s){
        LOGGER.info("实际传入的值为"+s);
        //设置对应传入的值
        DemoContext.setContext(s);
        CompletableFuture subThread = CompletableFuture.supplyAsync(()->{
            try{
                //打印子线程的值
                LOGGER.info("子线程的contextStr为:" + DemoContext.getContext());
            }catch (Throwable throwable){
                return throwable;
            }
            return null;
        });
        //打印主线程的值
        LOGGER.info("主线程的contextStr为:" + DemoContext.getContext());
        Throwable throwable = subThread.get();
        if (throwable!=null){
            throw throwable;
        }
        DemoContext.clearContext();
        return true;
    }

但是实际ThreadLocal本身,是针对每个线程实现单独数据存储的,并没有实现线程变量的传递,因而导致子线程无法获取到父线程的变量参数,从而导致业务逻辑代码本身出错。

2022-01-14 16:21:53.565  INFO 97654 --- [nio-8080-exec-2] c.example.demo.service.aop.TestService   : 实际传入的值为1
2022-01-14 16:21:55.331  INFO 97654 --- [nio-8080-exec-2] c.example.demo.service.aop.TestService   : 主线程的contextStr为:1
2022-01-14 16:21:55.331  INFO 97654 --- [onPool-worker-1] c.example.demo.service.aop.TestService   : 子线程的contextStr为:

改进一:InheritableThreadLocal

翻阅了网上的资料,了解到目前能够实现线程变量传递的方式主要是(ITL)和(TTL)两种方式,因而尝试性的使用了第一种方法,即采用ITL的方式实现。

代码改动主要如下:

public class DemoContext {
    ...
  
    //创建一个ThreadLocal
  private static final ThreadLocal CONTEXT_HOLDER = new InheritableThreadLocal<>();
        
  ...
}

经尝试,父子线程确实已经可以传递变量了,一下子安然自得不少~。同参数请求结果如下:

2022-01-14 16:40:30.476  INFO 98846 --- [nio-8080-exec-8] c.example.demo.service.aop.TestService   : 实际传入的值为: 1
2022-01-14 16:40:30.477  INFO 98846 --- [onPool-worker-5] c.example.demo.service.aop.TestService   : 子线程id=51,contextStr为:1
2022-01-14 16:40:30.477  INFO 98846 --- [nio-8080-exec-8] c.example.demo.service.aop.TestService   : 主线程id=46,contextStr为:1
2022-01-14 16:40:35.045  INFO 98846 --- [nio-8080-exec-9] c.example.demo.service.aop.TestService   : 实际传入的值为: 1
2022-01-14 16:40:35.045  INFO 98846 --- [nio-8080-exec-9] c.example.demo.service.aop.TestService   : 主线程id=48,contextStr为:1
2022-01-14 16:40:35.045  INFO 98846 --- [onPool-worker-5] c.example.demo.service.aop.TestService   : 子线程id=51,contextStr为:1
...

但是过了一阵时间后,发现出现了新的问题,子线程内携带的变量和主线程实际变量不一致,造成了业务数据查询混乱的问题。

2022-01-14 16:41:18.449  INFO 98846 --- [nio-8080-exec-1] c.example.demo.service.aop.TestService   : 实际传入的值为: 1
2022-01-14 16:41:18.449  INFO 98846 --- [nio-8080-exec-1] c.example.demo.service.aop.TestService   : 主线程id=37,contextStr为:1
2022-01-14 16:41:18.449  INFO 98846 --- [onPool-worker-6] c.example.demo.service.aop.TestService   : 子线程id=52,contextStr为:2

搜寻了相关文章内容研究发现,InheritableThreadLocal的原理是在子线程初始化的时候,将父线程的InheritableThreadLocal拷贝到子线程内。具体源码如下:

private void init(ThreadGroup g, Runnable target, String name,long stackSize, AccessControlContext acc,
                      boolean inheritThreadLocals) {
  ...
    //如果需要集成ThreadLocal 且父亲的InheritableThreadLocal不为空
        if (inheritThreadLocals && parent.inheritableThreadLocals != null)
           this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
  ...
}

但是问题在于,绝大多数的项目中会用到线程池,而线程池的工作机制就是【将当前工作的线程再次复用】,因此,线程池是不会进行线程初始化的调用的。也就导致了单纯使用InheritThreadLocal会出现数据污染的问题。

改进二:TransmittableThreadLocal

针对该问题,阿里的大佬们自行研发和开源了相应的组件TransmittableThreadLocal解决了这一痛点。

TransmittableThreadLocal继承了InheritThreadLocal类并对其进行的增强。

其使用主要有以下几种:

一、针对普通task执行的方式:

    @SneakyThrows
    public Boolean testNormalThreadTask(String s){
        LOGGER.info("实际传入的值为: " + s);
        //设置对应传入的值
        DemoContext.setContext(Integer.valueOf(s));
        Runnable runnable = () -> LOGGER.info(String.format("子线程id=%s,contextStr为:%s", Thread.currentThread().getId(), DemoContext.getContext()));
        //关键性代码,采用TtlRunnable进行装饰
        Runnable ttlRunnable = TtlRunnable.get(runnable);
        demoExecutor.submit(ttlRunnable);
        LOGGER.info(String.format("主线程id=%s,contextStr为:%s",Thread.currentThread().getId(),DemoContext.getContext()));
        return true;
    }

二、针对线程池的执行方式:

针对线程池,自然也是可以先修饰task,再调用线程池执行的方式。亦或者是通过对线程池进行包装,从而获取新的线程池变量。主要支持的包装方法有以下几个:

省去每次RunnableCallable传入线程池时的修饰,这个逻辑可以在线程池中完成。

通过工具类com.alibaba.ttl.threadpool.TtlExecutors完成,有下面的方法:

  • getTtlExecutor:修饰接口Executor
  • getTtlExecutorService:修饰接口ExecutorService
  • getTtlScheduledExecutorService:修饰接口ScheduledExecutorService

这里我以getTtlExecutor为例子,将对应的线程池进行包装后,发现问题得到解决。

    @Bean(name = "demoExecutor")
    public Executor demoExecutor() {
        ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor();
        threadPoolTaskExecutor.setCorePoolSize(5);
        threadPoolTaskExecutor.setQueueCapacity(0);
        threadPoolTaskExecutor.setKeepAliveSeconds(3600);
        threadPoolTaskExecutor.setMaxPoolSize(50);
        threadPoolTaskExecutor.setThreadNamePrefix("demoExecutor-");
        threadPoolTaskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        threadPoolTaskExecutor.setWaitForTasksToCompleteOnShutdown(true);
        threadPoolTaskExecutor.initialize();
        //对相应的线程池进行包装
        return TtlExecutors.getTtlExecutor(threadPoolTaskExecutor.getThreadPoolExecutor());
    }

三、针对java代码还有无侵入方式的解决方案

即借助于javaAgent实现的代理方式,这种方式能够对代码实现无侵入。

通过设置一个ThreadLocalAgent,来达到目的。

@Slf4j
public final class ThreadLocalAgent {

    public static void premain(String agentArgs, Instrumentation inst) {
        TtlAgent.premain(agentArgs, inst); // add TTL Transformer
    }
}

注意,在bootclasspath上,还是要加上TTL Jar

-Xbootclasspath/a:/path/to/transmittable-thread-local-2.x.y.jar:/path/to/your/agent/jar/files

更详细的步骤可以参考transmittable-thread-local

改进三:自定义装饰器

ThreadPoolTaskExecutor本身也是支持设置对应的装饰器的,因此,我们也可以对装饰器进行重载,在子线程进行runnable任务的时候,将父线程的Context变量传入到子线程的Context变量中,从而实现对应的变量传递。

public class GatewayHeaderTaskDecorator implements TaskDecorator {

    @Override
    public Runnable decorate(Runnable runnable) {
        // 获取父线程的DemoContext
        Integer contextInt = DemoContext.getContext();
        return () -> {
            try {
              // 添加到子线程中 完成拷贝
                DemoContext.setContext(contextInt);
                runnable.run();
            } finally {
                DemoContext.clearContext();
            }
        };
    }
}
2022-01-14 17:50:27.446  INFO 5969 --- [nio-8080-exec-2] c.example.demo.service.aop.TestService   : 实际传入的值为: 2
2022-01-14 17:50:27.451  INFO 5969 --- [onPool-worker-2] c.example.demo.service.aop.TestService   : 子线程id=64,contextStr为:2
2022-01-14 17:50:27.451  INFO 5969 --- [nio-8080-exec-2] c.example.demo.service.aop.TestService   : 主线程id=63,contextStr为:2
2022-01-14 17:50:31.135  INFO 5969 --- [nio-8080-exec-3] c.example.demo.service.aop.TestService   : 实际传入的值为: 2
2022-01-14 17:50:31.135  INFO 5969 --- [nio-8080-exec-3] c.example.demo.service.aop.TestService   : 主线程id=65,contextStr为:2
2022-01-14 17:50:31.135  INFO 5969 --- [onPool-worker-2] c.example.demo.service.aop.TestService   : 子线程id=64,contextStr为:2

参考文献

transmittable-thread-local

你可能感兴趣的:(父子线程变量传递,价值两个p0的代码修复)