多线程篇-父子线程的上下文传递

前言

在实际的工作中,有些时候我们会遇到一些线程之间共享数据的问题,比如一下几个典型的业务场景,举几个例子,大家能够更加直观的感受到

  1. 分布式链路追踪系统

    这个不必多说,如果我们需要记录系统的每个调用链路,那么每一个子系统里面,如果调用了异步线程来做处理的话,那么类似这种链路是不是需要收集起来呢?

  2. 日志收集记录系统上下文

    在实际的日志打印记录中,一个http请求进来的话,每一行日志,日志产生的线程信息?上下文信息,是不是需要记录下来呢?

上面我举的是我们最最最常见的两个例子了, 做了几年开发的都能理解为啥要有这个东西,下面我们仔细聊一下这个问题,

InheritableThreadLocal

其实说到这个问题,有些同学就会想到InheritableThreadLocal 这个工具了,这是JDK给我们提供的的工具,该工具可以解决父子线程之间的值的传递,我们先来一个简单的demo, 然后再进行原理分析

demo

/**
 * ce
 *
 * @author zhangyunhe
 * @date 2020-04-22 16:19
 */
public class InteritableTest2 {


    static ThreadLocal<String> local = new InheritableThreadLocal<>();
    // 初始化一个长度为1 的线程池
    static ExecutorService poolExecutor = Executors.newFixedThreadPool(1);

    public static void main(String[] args) throws ExecutionException, InterruptedException {

        InteritableTest2 test = new InteritableTest2();
        test.test();
    }
    private void test(){
        // 设置一个初始值
        local.set("天王老子");
        poolExecutor.submit(new Task());

    }

    class Task implements Runnable{

        @Override
        public void run() {
            // 子线程里面打印获取到的值
            System.out.println(Thread.currentThread().getName()+":"+local.get());
        }
    }
}

输出结果

pool-1-thread-1:天王老子

从上面可以看到, 子线程pool-1-thread-1可以获取到父线程在local里面设置的值,这就实现了值的传递了。

源码分析

下面我们从源码的角度上看一下InheritableThreadLocal的实现,他究竟是怎么做到父子线程之间线程的传递的。

我们首先看一下Thread创建的代码。

Thread

线程初始化的代码,可以看到重点在init方法

public Thread() {
        init(null, null, "Thread-" + nextThreadNum(), 0);
 }
private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize) {
        init(g, target, name, stackSize, null, true);
}
init

private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc,
                      boolean inheritThreadLocals) {
        if (name == null) {
            throw new NullPointerException("name cannot be null");
        }

        this.name = name;
            // 1. 获取当前线程为父线程,其实就是创建这个线程的线程
        Thread parent = currentThread();
                // 省略代码。。。。。
        // 2. 判断inheritThreadLocals 是否==true, 父节点的inheritableThreadLocals是否不为空
        if (inheritThreadLocals && parent.inheritableThreadLocals != null)
          //3. 符合以上的话,那么创建当前线程的inheritableThreadLocals
            this.inheritableThreadLocals =
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
        /* Stash the specified stack size in case the VM cares */
        this.stackSize = stackSize;

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

步骤说明:

看着我上面标的步骤进行说明

1.获取当前线程为父线程,其实就是创建这个线程的线程

2.判断inheritThreadLocals 是否==true , 默认inheritThreadLocals就是为true, 通用的new Thread()方法,这个值就是true, 同时判断父节点的inheritableThreadLocals是否为空, 如果不为空,则说明需要进行传递。

3.在这个if里面,针对当前线程做了inheritableThreadLocals的初始化, 把父线程的值拷贝到这个里面来。

通过上面的分析,其实基本的原理都已经了解清楚了,不熟悉的可以可以自己去细细研究。

那么是否这种做法完全可以符合我们的需求呢? 我们看一下下面的场景

线程池异常场景

线程池demo

/**
 * ce
 *
 * @author zhangyunhe
 * @date 2020-04-22 16:19
 */
public class InteritableTest {


    static ThreadLocal<String> local = new InheritableThreadLocal<>();

    static ExecutorService poolExecutor = Executors.newFixedThreadPool(1);

    public static void main(String[] args) throws ExecutionException, InterruptedException {

        InteritableTest test = new InteritableTest();
        test.test();
    }
    private void test() throws ExecutionException, InterruptedException {


        local.set("天王老子");
        Future future = poolExecutor.submit(new Task("任务1"));

        future.get();

        Future future2 = poolExecutor.submit(new Task("任务2"));

        future2.get();

        System.out.println("父线程的值:"+local.get());
    }

    class Task implements Runnable{

        String str;
        Task(String str){
            this.str = str;
        }
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName()+":"+local.get());
            local.set(str);
            System.out.println(local.get());
        }
    }

}

输出结果:

pool-1-thread-1:天王老子
任务1
pool-1-thread-1:任务1
任务2
父线程的值:天王老子

从上面可以看到,Task2执行的时候,获取到的父线程的值是Task1修改过的。 这样感觉是不是就破坏了我们的本意? 实际上,这是因为我们使用了线程池,在池化技术里面,线程是会被复用的,当执行Task2的时候,实际上是用的Task1的那个线程,那个线程已经被创建好了的,所以那里面的locals就是被Task1修改过的,那么遇到这种问题,该如何解决呢?

下一篇文章给大家介绍一个组件,在使用线程池等会池化复用线程的执行组件情况下,提供ThreadLocal值的传递功能

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

推荐阅读更多精彩内容