一、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; }
該方法首先從當前線程中獲取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); }
該方法同樣獲取當前線程的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()); }
運行結果:

父線程獲取到的值:dhytest 子線程獲取到的值:null
如何將父線程的值傳遞給子線程?
方法一:
在執行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() + "---子線程對父線程的值做了修改" ); } }
運行結果:
原父線程的值: 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() + "---子線程對父線程的值做了修改"); } }
運行結果:

子線程中直接調用get方法獲取父線程的值,value:dhyTest
父線程獲取到的最終的值:dhyTest---子線程對父線程的值做了修改
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); } }
再看下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(); }
具體看下是子線程是如何拷貝父線程的值的:

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++; } } } }
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("父線程的值也被修改啦,因為是引用傳遞")); } }
運行結果:

任務【task1】獲取到父線程的值為:dhytest
任務【task2】獲取到父線程的值為:值被任務【task1】修改啦
父線程的值:dhytest
為什么第二個任務獲取到的是第一個任務設置的值,而沒有獲取到父線程原本的值?
從實例化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(); }
運行結果:

任務【task1-parent1】獲取到父線程的值為:parent1
任務【task2-parent2】獲取到父線程的值為:parent1
從結果中可以看出任務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(); } } }
調用方式:

executorService.submit(new ExtRunnable(new ChildThread("task1-parent1")));
方法三:
阿里封裝了一個工具,實現了在使用線程池等會池化復用線程的組件情況下,提供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();