多線程篇-父子線程的上下文傳遞

前言

在實際的工作中,有些時候我們會遇到一些線程之間共享數(shù)據(jù)的問題,比如一下幾個典型的業(yè)務場景,舉幾個例子,大家能夠更加直觀的感受到

  1. 分布式鏈路追蹤系統(tǒng)

    這個不必多說,如果我們需要記錄系統(tǒng)的每個調(diào)用鏈路,那么每一個子系統(tǒng)里面,如果調(diào)用了異步線程來做處理的話,那么類似這種鏈路是不是需要收集起來呢?

  2. 日志收集記錄系統(tǒng)上下文

    在實際的日志打印記錄中,一個http請求進來的話,每一行日志,日志產(chǎn)生的線程信息?上下文信息,是不是需要記錄下來呢?

上面我舉的是我們最最最常見的兩個例子了, 做了幾年開發(fā)的都能理解為啥要有這個東西,下面我們仔細聊一下這個問題,

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());
        }
    }
}

輸出結(jié)果

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

從上面可以看到, 子線程pool-1-thread-1可以獲取到父線程在local里面設置的值,這就實現(xiàn)了值的傳遞了。

源碼分析

下面我們從源碼的角度上看一下InheritableThreadLocal的實現(xiàn),他究竟是怎么做到父子線程之間線程的傳遞的。

我們首先看一下Thread創(chuàng)建的代碼。

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. 獲取當前線程為父線程,其實就是創(chuàng)建這個線程的線程
        Thread parent = currentThread();
                // 省略代碼。。。。。
        // 2. 判斷inheritThreadLocals 是否==true, 父節(jié)點的inheritableThreadLocals是否不為空
        if (inheritThreadLocals && parent.inheritableThreadLocals != null)
          //3. 符合以上的話,那么創(chuàng)建當前線程的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.獲取當前線程為父線程,其實就是創(chuàng)建這個線程的線程

2.判斷inheritThreadLocals 是否==true , 默認inheritThreadLocals就是為true, 通用的new Thread()方法,這個值就是true, 同時判斷父節(jié)點的inheritableThreadLocals是否為空, 如果不為空,則說明需要進行傳遞。

3.在這個if里面,針對當前線程做了inheritableThreadLocals的初始化, 把父線程的值拷貝到這個里面來。

通過上面的分析,其實基本的原理都已經(jīng)了解清楚了,不熟悉的可以可以自己去細細研究。

那么是否這種做法完全可以符合我們的需求呢? 我們看一下下面的場景

線程池異常場景

線程池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());
        }
    }

}

輸出結(jié)果:

pool-1-thread-1:天王老子
任務1
pool-1-thread-1:任務1
任務2
父線程的值:天王老子

從上面可以看到,Task2執(zhí)行的時候,獲取到的父線程的值是Task1修改過的。 這樣感覺是不是就破壞了我們的本意? 實際上,這是因為我們使用了線程池,在池化技術(shù)里面,線程是會被復用的,當執(zhí)行Task2的時候,實際上是用的Task1的那個線程,那個線程已經(jīng)被創(chuàng)建好了的,所以那里面的locals就是被Task1修改過的,那么遇到這種問題,該如何解決呢?

下一篇文章給大家介紹一個組件,在使用線程池等會池化復用線程的執(zhí)行組件情況下,提供ThreadLocal值的傳遞功能

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容