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

歡迎關注公衆號【sharedCode】致力於主流中間件的源碼分析, 我的網站:https://www.shared-code.com/java

前言

在實際的工做中,有些時候咱們會遇到一些線程之間共享數據的問題,好比一下幾個典型的業務場景,舉幾個例子,你們可以更加直觀的感覺到異步

  1. 分佈式鏈路追蹤系統分佈式

    這個沒必要多說,若是咱們須要記錄系統的每一個調用鏈路,那麼每個子系統裏面,若是調用了異步線程來作處理的話,那麼相似這種鏈路是否是須要收集起來呢?ide

  2. 日誌收集記錄系統上下文工具

    在實際的日誌打印記錄中,一個http請求進來的話,每一行日誌,日誌產生的線程信息?上下文信息,是否是須要記錄下來呢?源碼分析

上面我舉的是咱們最最最多見的兩個例子了, 作了幾年開發的都能理解爲啥要有這個東西,下面咱們仔細聊一下這個問題,網站

InheritableThreadLocal

其實說到這個問題,有些同窗就會想到InheritableThreadLocal 這個工具了,這是JDK給咱們提供的的工具,該工具能夠解決父子線程之間的值的傳遞,咱們先來一個簡單的demo, 而後再進行原理分析this

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值的傳遞功能

相關文章
相關標籤/搜索