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

问题复现:

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

public class DemoContext {
    ...
  
    //创建一个ThreadLocal
  private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();
        
  ...
}
    @SneakyThrows
    public Boolean testThreadLocal(String s){
        LOGGER.info("实际传入的值为"+s);
        //设置对应传入的值
        DemoContext.setContext(s);
        CompletableFuture<Throwable> 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<String> 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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,711评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,932评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,770评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,799评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,697评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,069评论 1 276
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,535评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,200评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,353评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,290评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,331评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,020评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,610评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,694评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,927评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,330评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,904评论 2 341

推荐阅读更多精彩内容