JAVA进阶篇(8)—TransmittableThreadLocal—父子线程间线程本地变量

1. 传统的ThreadLocal

ThreadLocal是线程本地变量。即每一个线程中都会存在一个ThreadLocal对象。

ThreadLocal.set()方法:获取当前线程中的ThreadLocalMap对象,将ThreadLocal对象作为key,set()的值作为value,放入到线程中ThreadLocalMap中。

问题:若是在线程中开启子线程,那么子线程也会存在一个ThreadLocalMap对象,但是它不存在父线程ThreadLocalMap对象中的值。

@Slf4j
public class testThreadLocal {

    @Test
    public void testThreadLocal2() throws InterruptedException {

        ThreadLocal<String> local = new ThreadLocal<>();
        try {
            local.set("我是主线程");
            ExecutorService executorService = Executors.newFixedThreadPool(1);

            CountDownLatch c1 = new CountDownLatch(1);
            CountDownLatch c2 = new CountDownLatch(1);

            executorService.execute(() -> {
                System.out.println("线程1" + local.get());
                c1.countDown();
            });
            c1.await();
            executorService.execute(() -> {
                System.out.println("线程2" + local.get());
                c2.countDown();
            });
            c2.await();
            executorService.shutdownNow();
        } finally {
            //使用完毕,清除线程中ThreadLocalMap中的key。
            local.remove();
        }
    }
}

执行结果:

线程1null
线程2null

2. 可继承的InheritableThreadLocal

image.png

InheritableThreadLocal是ThreadLocal的子类,字面意义上是子线程可以继承父线程中的值。

案例1:

@Slf4j
public class testThreadLocal {
    @Test
    public void testInheritableThreadLocal() throws InterruptedException {
        ThreadLocal<String> local = new InheritableThreadLocal<>();
        local.set("我是主线程");
        new Thread(() -> {
            System.out.println("子线程1" + local.get());
        }).start();
        Thread.sleep(2000);
    }
}

执行结果:

子线程1我是主线程

可以看到,子线程ThreadLocalMap也持有父线程ThreadLocalMap的值。

源码简介:

public class InheritableThreadLocal<T> extends ThreadLocal<T> {
   ...
    /**
     * 创建Map的时候,会初始化当前线程中的inheritableThreadLocals变量。
     *
     * @param t 当前的线程。
     * @param firstValue 初始化table的第一个值。
     */
    void createMap(Thread t, T firstValue) {
        t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
    }
}
public
class Thread implements Runnable {
    /*
     * 与此线程相关的InheritableThreadLocal值。这map是由InheritableThreadLocal类维护。
     */
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

    //线程初始化
    private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc,
                      boolean inheritThreadLocals) {
        ...
        //如果inheritThreadLocals为true且父类存在inheritableThreadLocals,那么
        //子类将继承父类的inheritableThreadLocals的值。
        if (inheritThreadLocals && parent.inheritableThreadLocals != null)
            this.inheritableThreadLocals =
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
        /* Stash the specified stack size in case the VM cares */
        this.stackSize = stackSize;

        /* Set thread ID */
        tid = nextThreadID();
    }
}

由源码可知,线程只有在初始化的时候才会继承父类ThreadLocal的值。也就是若是线程池的话,以及子类依旧不能继承父类的ThreadLocal的值。

实验:

@Slf4j
public class testThreadLocal {

    @Test
    public void testThreadLocal2() throws InterruptedException {

        ThreadLocal<String> local = new InheritableThreadLocal<>();
        try {
            local.set("我是主线程");
            ExecutorService executorService = Executors.newFixedThreadPool(1);

            CountDownLatch c1 = new CountDownLatch(1);
            CountDownLatch c2 = new CountDownLatch(1);
            //初始化init的时候,赋予了父线程的ThreadLocal的值
            executorService.execute(() -> {
                System.out.println("线程1" + local.get());
                c1.countDown();
            });
            c1.await();
            //主线程修改值
            local.set("修改主线程");
            //再次调用,查看效果
            executorService.execute(() -> {
                System.out.println("线程2" + local.get());
                c2.countDown();
            });
            c2.await();
            executorService.shutdownNow();
        } finally {
            //使用完毕,清除线程中ThreadLocalMap中的key。
            local.remove();
        }
    }
}

执行结果:

线程1我是主线程
线程2我是主线程

可以看到,线程池只会初始化一次,依旧存在问题。

3. 最终解决方案TransmittableThreadLocal

阿里巴巴开源解决方案:https://github.com/alibaba/transmittable-thread-local

<dependency>
     <groupId>com.alibaba</groupId>
     <artifactId>transmittable-thread-local</artifactId>
     <version>2.11.5</version>
     <scope>compile</scope>
</dependency>

3.1 业务侵入式

alibaba提供了一系列的包装类,可以对ExecutorServic及其子类进行装饰,由TtlExecutors类实现。

    @Test
    public void testInheritableThreadLocal3() throws InterruptedException {
        TransmittableThreadLocal<String> local = new TransmittableThreadLocal<>();
        local.set("我是主线程");
        //生成额外的代理

        ExecutorService executorService = Executors.newFixedThreadPool(1);
        //核心装饰代码
        executorService = TtlExecutors.getTtlExecutorService(executorService);

        CountDownLatch c1 = new CountDownLatch(1);
        CountDownLatch c2 = new CountDownLatch(1);

        executorService.submit(() -> {
            System.out.println("我是线程1:" + local.get());
            c1.countDown();
        });
        c1.await();
        local.set("修改主线程");
        System.out.println(local.get());
        executorService.submit(() -> {
            System.out.println("我是线程2:" + local.get());
            c2.countDown();
        });
        c2.await();
    }

3.2 agent实现

需配置启动参数-javaagent:path/to/transmittable-thread-local-2.x.x.jar

    @Test
    public void testTTL() throws InterruptedException {
        TransmittableThreadLocal<String> local = new TransmittableThreadLocal<>();
        local.set("我是主线程");
        //生成额外的代理

        CountDownLatch c1 = new CountDownLatch(1);
        CountDownLatch c2 = new CountDownLatch(1);

        CompletableFuture.supplyAsync(() -> {
            System.out.println("开启了线程1:" + local.get());
            c1.countDown();
            return "线程1的值";
        });
        c1.await();
        local.set("修改主线程");
        System.out.println(local.get());

        CompletableFuture.supplyAsync(() -> {
            System.out.println("开启了线程2:" + local.get());
            c1.countDown();
            return "线程2的值";
        });
        c2.await();
        System.out.println("我是主线程:" + local.get());
    }

执行结果:

开启了线程1:我是主线程
修改主线程
开启了线程2:修改主线程
idea修改配置.png

在ThreadLocal的需求场景即是TransmittableThreadLocal的潜在需求场景,如果你的业务需要『在使用线程池等会池化复用线程的执行组件情况下传递ThreadLocal』则是TransmittableThreadLocal目标场景。

下面是几个典型场景例子。

分布式跟踪系统 或 全链路压测(即链路打标)
日志收集记录系统上下文
Session级Cache
应用容器或上层框架跨应用代码给下层SDK传递信息

JAVA并发(4)— ThreadLocal源码角度分析是否真正能造成内存溢出!

推荐阅读

https://www.cnblogs.com/Nonnetta/p/10175662.html

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念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