父線程與子線程傳值問題


一、ThreadLocal回顧

ThreadLocal對象用於在同一個線程中傳遞數據,避免顯式的在方法中傳參。

每個線程中保存了ThreadLocalMap對象,ThreadLocalMap對象的key就是ThreadLocal對象本身,value就是當前線程的值。

看下ThreadLocal的get方法

public T get() {
        //當前線程
        Thread t = Thread.currentThread();
        //獲取當前線程的ThreadLocalMap對象
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            //獲取該ThreadLocal對象的value
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        //設置初始值
        return setInitialValue();
    }
    
    //獲取當前線程的ThreadLocalMap對象
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
View Code

該方法首先從當前線程中獲取ThreadLocalMap對象,接着從ThreadLocalMap獲取該ThreadLocal鎖對應的值;如果未獲取到,調用setInitialValue方法,設置初始值,並返回初始值。再看下ThreadLocal的set方法

public void set(T value) {
        //獲取當前線程
        Thread t = Thread.currentThread();
        //獲取當前線程的ThreadLocalMap對象
        ThreadLocalMap map = getMap(t);
        //如果ThreadLocalMap對象存在,則直接設置key(ThreadLocal對象),value;否則創建ThreadLocalMap對象,並設置key,value
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }
    
     void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
View Code

該方法同樣獲取當前線程的ThreadLocalMap對象,如果該對象不為空,那么設置key(ThreadLocal對象),value;否則創建ThreadLocalMap對象,並設置key,value

二、父線程與子線程傳值問題

ThreadLocal無法將父線程中的值傳遞到子線程

下面的代碼在主線程中設置threadLocal的值為"dhytest",在子線程中調用get方法,聰明的你一定知道返回的是null. 因為在子線程中調用get方法,獲取的是子線程中的ThreadLocalMap對象,而子線程中的ThreadLocalMap對象並未對key (threadLocal)設置相應的value

static ThreadLocal<String> threadLocal = new ThreadLocal<>();

public static void main(String[] args) {

    threadLocal.set("dhytest");
    
    new Thread(()->{
        System.out.println("子線程獲取到的值:" + threadLocal.get());
    }).start();

    System.out.println("父線程獲取到的值:" + threadLocal.get());
}
View Code

運行結果:

父線程獲取到的值:dhytest
子線程獲取到的值:null
View Code

如何將父線程的值傳遞給子線程?

方法一:

在執行start方法前獲取到父線程的值,因為在thread對象執行start方法前,當前線程還是父線程,因此可以通過threadLocal.get方法獲取父線程的值

static ThreadLocal<String> threadLocal = new ThreadLocal<>();
   
   private void test() throws InterruptedException {

        Thread.currentThread().setName("main-thread");

        //主線程設置一個值
        threadLocal.set(new Value("dhyTest"));

        //運行子線程
        Thread childThread = new Thread(new ParentChildTransferValue2.ChildThread(), "child-thread");
        childThread.start();

        //主線成等待子線程運行完,以便觀察主線中設置的值是否被子線程成功修改
        childThread.join();

        System.out.println("父線程獲取到的最終的值:" + threadLocal.get());

    }

    class ChildThread implements Runnable {
        //獲取主線程中設置的值
        Value value = threadLocal.get();

        @Override
        public void run() {

            //打印主線程的值
            System.out.println("原父線程的值: " + value);

            //如果啟用了線程(調用start方法),調用get方法是獲取不到值的
            Value nullValue = threadLocal.get();
            System.out.println("子線程中直接調用get方法獲取父線程的值,value:" + nullValue);

            //獲取到父線程的值,並進行更改
            value.setData(value.getData() + "---子線程對父線程的值做了修改" );
        }
    }
View Code

運行結果:

原父線程的值: dhyTest
子線程中直接調用get方法獲取父線程的值,value:null
父線程獲取到的最終的值:dhyTest---子線程對父線程的值做了修改

方法二

使用 InheritableThreadLocal

static InheritableThreadLocal<Value> threadLocal = new InheritableThreadLocal<>();

    private void test() throws InterruptedException {

        Thread.currentThread().setName("main-thread");

        //主線程設置一個值
        threadLocal.set(new Value("dhyTest"));

        //運行子線程
        Thread childThread = new Thread(new ChildThread(), "child-thread");
        childThread.start();

        //主線成等待子線程運行完,以便觀察主線中設置的值是否被子線程成功修改
        childThread.join();

        System.out.println("父線程獲取到的最終的值:" + threadLocal.get());

    }

    class ChildThread implements Runnable {
        @Override
        public void run() {
            Value value = threadLocal.get();
            System.out.println("子線程中直接調用get方法獲取父線程的值,value:" + value);
            value.setData(value.getData() + "---子線程對父線程的值做了修改");
        }
    }
View Code

運行結果:

子線程中直接調用get方法獲取父線程的值,value:dhyTest
父線程獲取到的最終的值:dhyTest---子線程對父線程的值做了修改
View Code

InheritableThreadLocal分析

為什么使用InheritableThreadLocal,子線程就可以獲取到父線程的值

看下InheritableThreadLocal類,InheritableThreadLocal繼承了ThreadLocal類,重寫了childValue,getMap,createMap方法

對於getMap方法,InheritableThreadLocal中返回的是線程中的inheritableThreadLocals變量,而ThreadLocal返回的是線程中的threadLocals變量;setMap同理

public class InheritableThreadLocal<T> extends ThreadLocal<T> {
   
    protected T childValue(T parentValue) {
        return parentValue;
    }
    
    ThreadLocalMap getMap(Thread t) {
       return t.inheritableThreadLocals;
    }

    void createMap(Thread t, T firstValue) {
        t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
    }
}
View Code

再看下Thread實例化的代碼,從構造函數跟進init方法,inheritThreadLocals變量是true。在init方法中,獲取父線程,將父線程的inheritableThreadLocals變量賦值給子線程的inheritableThreadLocals變量,從而實現了父線程與子線程的傳值

public Thread(Runnable target) {
        init(null, target, "Thread-" + nextThreadNum(), 0);
    }
    
    private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize) {
        init(g, target, name, stackSize, null, true);
    }
    
    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;
                //這里獲取了父線程
        Thread parent = currentThread();
        SecurityManager security = System.getSecurityManager();
        
        //中間省略了一些代碼
        
        //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();
    }
View Code

具體看下是子線程是如何拷貝父線程的值的:

static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
        return new ThreadLocalMap(parentMap);
    }
    
    private ThreadLocalMap(ThreadLocalMap parentMap) {
            Entry[] parentTable = parentMap.table;
            int len = parentTable.length;
            setThreshold(len);
            table = new Entry[len];

            for (int j = 0; j < len; j++) {
                Entry e = parentTable[j];
                if (e != null) {
                    @SuppressWarnings("unchecked")
                    ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
                    if (key != null) {
                        Object value = key.childValue(e.value);
                        Entry c = new Entry(key, value);
                        int h = key.threadLocalHashCode & (len - 1);
                        //如果發生hash碰撞,那么槽位后移
                        while (table[h] != null)
                            h = nextIndex(h, len);
                        table[h] = c;
                        size++;
                    }
                }
            }
        }
View Code

InheritableThreadLocal存在的問題

雖然InheritableThreadLocal可以解決在子線程中獲取父線程的值的問題,但是在使用線程池的情況下,由於不同的任務有可能是同一個線程處理,因此這些任務取到的值有可能並不是父線程設置的值

case1:模擬使用線程池情況下(為了便於測試不同任務有同一個線程處理的場景,使用單線程),兩個任務由同一個線程處理。

第一個任務中獲取到父線程的值,並且重新設置了值;第二個任務中獲取到的並不是父線程的值了,而是第一個任務設置的值。

static InheritableThreadLocal<Value> threadLocal = new InheritableThreadLocal<>();
static ExecutorService executorService = Executors.newSingleThreadExecutor();

private void test() throws InterruptedException, ExecutionException {

        threadLocal.set(new Value("dhytest"));

        Future<?> task1 = executorService.submit((new ChildThread("task1")));
        task1.get();

        Future<?> task2 = executorService.submit(new ChildThread("task2"));
        task2.get();

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

    class ChildThread implements Runnable {

        private String taskName;

        ChildThread(String taskName) {
            this.taskName = taskName;
        }

        @Override
        public void run() {
            Value value = threadLocal.get();
            System.out.println("任務【" + taskName + "】獲取到父線程的值為:" + value);
            threadLocal.set(new Value("值被任務【" + taskName + "】修改啦"));
            //如果使用下面的代碼,那么父線程的值是會被改變的
            //value.set(new Value("父線程的值也被修改啦,因為是引用傳遞"));
        }
    }
View Code

運行結果:

任務【task1】獲取到父線程的值為:dhytest
任務【task2】獲取到父線程的值為:值被任務【task1】修改啦
父線程的值:dhytest
View Code

為什么第二個任務獲取到的是第一個任務設置的值,而沒有獲取到父線程原本的值?

從實例化Thread的方法(init)中可以看出,實例化線程時,會檢測是否需滿足拷貝父線程的條件(inheritThreadLocals 是true並且父線程的inheritableThreadLocals不為空),若果滿足,那么將父線程的inheritableThreadLocals變量拷貝給子線程的inheritableThrealLocals變量,也就是Thread類的ThreadLocal.createInheritedMap方法。

執行第一個任務時,創建一個線程,執行初始化,將父線程的inheritableThreadLocals拷貝給了子任務;調用get(InheritableThreadLocal 繼承了ThreadLocal,重寫了getMap方法)方法,會返回給線程持有的inheritableThreadLocals變量;

執行第二個任務時,由於使用的是同一個線程,因此調用get方法,返回的是這個線程持有的inheritableThreadLocals變量,而此時該變量中的value已被第一個任務改寫,因此獲取到並不是父線程原本的值

雖然任務對value進行了重新賦值,但是並不影響父線程的值,因為value指向了一個新的地址。如果直接更改value,那么會影響父線程的值,因為指向的是同一個地址

case2:使用線程池情況下,子任務由同一個線程處理,但是父線程是不同的線程

private void test() throws InterruptedException, ExecutionException {
        //父線程1
        Thread parent1 = new Thread(() -> {
            threadLocal.set(new Value("parent1"));
            Future<?> task1 = executorService.submit((new ChildThread("task1-parent1")));
            try {
                task1.get();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (ExecutionException e) {
                e.printStackTrace();
            }
        });
        parent1.start();
        parent1.join();
        //父線程2
        Thread parent2 = new Thread(() -> {
            threadLocal.set(new Value("parent2"));
            Future<?> task2 = executorService.submit((new ChildThread("task2-parent2")));
            try {
                task2.get();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (ExecutionException e) {
                e.printStackTrace();
            }
        });
        parent2.start();

    }
View Code

運行結果:

任務【task1-parent1】獲取到父線程的值為:parent1
任務【task2-parent2】獲取到父線程的值為:parent1
View Code

從結果中可以看出任務2沒有獲取到父線程的值,而是獲取到任務1的父線程的值,原因其實和case1差不多,本質原因都是因為任務1和任務2使用的是同一個線程,因此get的是同一個value

在使用線程池時,如何在子線程中正確的獲取父線程的值?

既然問題的根源是由於使用同一個線程造成的,那么在任務執行完后,清空該線程持有的threadLocals或者inheritableThreadLocals中的value,執行其他任務時,能夠重新拷貝父線程的值就好了

如何實現?

1.如前面的方法一,在執行任務前,備份父線程的值,任務結束后,清除該子線程的值

擴展下,可以使用裝飾器模式來裝飾我們的任務。首先在任務執行備份父線程的值;在任務執行時,拷貝父線程的值到子線程;任務執行結束后,清除子線程持有的備份數據

 
public class ExtRunnable implements Runnable {

    //父線程的值
    Value value = AppContext.get();

    private Runnable runnable;

    public ExtRunnable(Runnable runnable) {
        this.runnable = runnable;
    }

    @Override
    public void run() {
        //將父線程的值拷貝的子線程
        AppContext.set(value);
        try {
            this.runnable.run();
        } finally {
            //任務執行完后,將該子線程的值刪除
            AppContext.remove();
        }
    }
}
View Code

調用方式:

executorService.submit(new ExtRunnable(new ChildThread("task1-parent1")));
View Code

方法三:

阿里封裝了一個工具,實現了在使用線程池等會池化復用線程的組件情況下,提供ThreadLocal值的傳遞功能,解決異步執行時上下文傳遞的問題

https://github.com/alibaba/transmittable-thread-local

官網中給出的示例代碼 :

TransmittableThreadLocal<String> parent = new TransmittableThreadLocal<String>();
parent.set("value-set-in-parent");

Runnable task = new Task("1");
// 額外的處理,生成修飾了的對象ttlRunnable
Runnable ttlRunnable = TtlRunnable.get(task);
executorService.submit(ttlRunnable);

// =====================================================

// Task中可以讀取,值是"value-set-in-parent"
String value = parent.get();
View Code

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM