今天聊一聊并發(fā)編程中經(jīng)常遇到也是面試時(shí)容易被為難的一個(gè)題目,線程間局部變量的傳遞問(wèn)題。
相信對(duì)并發(fā)編程有一定了解的同學(xué)已經(jīng)想到了大名鼎鼎的 ThreadLocal 了,是的,線程內(nèi)部就是通過(guò) inheritableThreadLocals 實(shí)現(xiàn)了父子線程間局部變量的傳遞。
JDK 8
首先我們寫(xiě)看一段代碼:
public class ThreadLocalTest implements Runnable{ private static final InheritableThreadLocal<String> MAIN_THREAD_LOCAL = new InheritableThreadLocal<>(); @SneakyThrows @Override public void run() { System.out.println("threadlocal 默認(rèn)值:"+ThreadLocalTest.MAIN_THREAD_LOCAL.get()); MAIN_THREAD_LOCAL.set("child thread value :"+Thread.currentThread().getName()); System.out.println("threadlocal 設(shè)置子線程值之后:"+ThreadLocalTest.MAIN_THREAD_LOCAL.get()); } public String get(){ return MAIN_THREAD_LOCAL.get(); } public void clean(){ MAIN_THREAD_LOCAL.remove(); } public static void main(String[] args) { ThreadLocalTest threadLocalTest = new ThreadLocalTest(); MAIN_THREAD_LOCAL.set("父線程的值 set 111"); System.out.println("啟動(dòng):"+threadLocalTest.get()); for (int i = 0; i < 3; i++) { new Thread(threadLocalTest).start();// ThreadUtil.execAsync(threadLocalTest); } System.out.println("結(jié)束:"+threadLocalTest.get()); }}
在上面的這段代碼中,我們就做了三個(gè)事情:
大家可以先猜一下這段代碼的運(yùn)行結(jié)果。
首先我們先說(shuō)下答案,是可以繼承的。上面代碼的執(zhí)行結(jié)果如下:
啟動(dòng):父線程的值 set 111結(jié)束:父線程的值 set 111threadlocal 默認(rèn)值:父線程的值 set 111threadlocal 設(shè)置子線程值之后:child thread value :Thread-1threadlocal 默認(rèn)值:父線程的值 set 111threadlocal 默認(rèn)值:父線程的值 set 111threadlocal 設(shè)置子線程值之后:child thread value :Thread-2threadlocal 設(shè)置子線程值之后:child thread value :Thread-0
在上面的代碼中,我們的子線程優(yōu)先打印了父線程中ThreadLocal的值,然后重新設(shè)置該值,再次讀取。得出結(jié)論就是子線程可以通過(guò)ThreadLocal繼承父線程的值,并且子線程自己內(nèi)容再次重新設(shè)置不影響父線程的值。
難道一句簡(jiǎn)單的ThreadLocal就可以讓我們對(duì)這個(gè)問(wèn)題停止探索嗎?那么線程內(nèi)部是如何通過(guò)ThreadLocal進(jìn)行傳值的呢?
在上面代碼中,啟動(dòng)子線程的方式是new Thread(threadLocalTest).start();,所以秘密一定就在這一行代碼里面。源碼之下無(wú)秘密,我們一起來(lái)看下。
首先進(jìn)入new Thread()的內(nèi)部:
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); }
通過(guò)上面兩個(gè)方法調(diào)用,最終進(jìn)入到下面這個(gè)方法中:
private void init(ThreadGroup g, Runnable target, String name, long stackSize, AccessControlContext acc, boolean inheritThreadLocals) {}
init方法有個(gè)參數(shù)inheritThreadLocals,boolean類型的,如果為true,且可繼承的線程局部變量不為空就繼承。
現(xiàn)在我們只需要順著inheritThreadLocals這個(gè)參數(shù)去找就可以了,在Thread的418行,有這樣一行代碼。(代碼行數(shù)可能因版本而位置不同)
可以看到是直接對(duì)當(dāng)前線程的inheritableThreadLocals直接進(jìn)行的賦值操作,而值是通過(guò)ThreadLocal.createInheritedMap獲取的,下面我們看下這個(gè)createInheritedMap方法做了哪些操作?
createInheritedMap方法是ThredLocal內(nèi)部的方法,接收傳遞父線程的ThreadLocalMap為參數(shù),該方法只做了一個(gè)事情,就是new了一個(gè)新的ThreadLocalMap。
跟進(jìn)到new ThreadLocalMap(parentMap)方法內(nèi)部,其實(shí)是把傳進(jìn)的值,一個(gè)個(gè)的遍歷進(jìn)行賦值到當(dāng)前線程中。
對(duì)于圖中標(biāo)記的第二個(gè)地方,childValue調(diào)用的是InheritableThreadLocal#childValue,該方法內(nèi)也只做了一件事,就是返回傳進(jìn)來(lái)的值。
(1) 小結(jié)
父子線程之所以能傳參,是因?yàn)槲覀兪褂昧薎nheritableThreadLocal,這樣在new Thread()時(shí),就會(huì)進(jìn)入到給子線程賦值父線程inheritableThreadLocals的邏輯中去。
(2) 擴(kuò)展
有的同學(xué)會(huì)說(shuō)了,我用 ThreadLocal.withInitial創(chuàng)建的,怎么走到線程的if (inheritThreadLocals && parent.inheritableThreadLocals != null)判斷時(shí),沒(méi)有進(jìn)去呢,上面不是說(shuō)是在這判斷然后對(duì)子線程進(jìn)行賦值的嗎?
在這簡(jiǎn)單說(shuō)一下哈,大家在寫(xiě)代碼時(shí),或者再用第三方框架時(shí),源碼中的注釋一定要看仔細(xì),很多細(xì)節(jié)都在注釋中標(biāo)注清楚了。
public static ThreadLocal<String> MAIN_THREAD_LOCAL = ThreadLocal.withInitial(() -> "父線程的值 withInitial 111");
在上面的代碼中,我們進(jìn)行了ThreadLocal的初始化賦值,然后看下withInitial方法。
所以是當(dāng)調(diào)用get方法時(shí),才會(huì)觸發(fā)賦值的操作,那么我們看下get方法。
如果當(dāng)前線程的局部變量沒(méi)有值,返回初始化方法初始的值。
所以對(duì)于我們來(lái)說(shuō)就是SuppliedThreadLocal#initialValue返回的值。
剛才我們是通過(guò)new Thread()啟動(dòng)的子線程,可是工作中基本都是通過(guò)線程池的方式執(zhí)行任務(wù)的啊,那還生效嗎?
答案是生效。
我們使用hutool工具中的ThreadUtil.execAsync(threadLocalTest);進(jìn)行測(cè)試。
直接說(shuō)結(jié)論,感興趣的同學(xué)可以自行修改一下代碼中的子線程啟動(dòng)方式。
先畫(huà)個(gè)流程圖,大家可以跟著代碼走一下。
當(dāng)使用線程池時(shí),底層原理還是線程池中放入任務(wù)的邏輯,當(dāng)放入線程池之后,會(huì)在AbstractExecutorService#submit()方法中執(zhí)行execute方法,最終執(zhí)行在ThreadPoolExecutor#execute(),在這里,就是把任務(wù)丟入線程池工作的邏輯,其中有個(gè)方法addWorker,該方法中有一行new Worker(),而在該Worker方法的內(nèi)部,其實(shí)就是new Thread(),到了這,就與上面所說(shuō)的一樣了,到了判斷inheritableThreadLocals的時(shí)候了。
使用ThreadLocal的應(yīng)用場(chǎng)景有很多,父子線程傳參數(shù)的場(chǎng)景也有不少,但是有一個(gè)很關(guān)鍵的點(diǎn)內(nèi)存溢出是需要重視的。解決ThreadLocal內(nèi)存溢出的方式也很簡(jiǎn)單,就是在使用完成之后調(diào)用一下remove。
對(duì)于上面的代碼示例,就是調(diào)用我們的clean方法。
public void clean(){ MAIN_THREAD_LOCAL.remove();}
remove的代碼如下,取值不為null時(shí),執(zhí)行刪除邏輯。
我們通過(guò)一個(gè)示例,驗(yàn)證了父子線程間可以通過(guò)ThreadLocal進(jìn)行傳遞,測(cè)試了不同方式初始化ThreadLocal,并對(duì)比了new Thread()與線程池啟動(dòng)的區(qū)別。
其實(shí)殊途同歸,線程池最后調(diào)用的還是Thread里面的方法。唯一需要注意的就是通過(guò)ThreadLocal.withInitial初始化是在get時(shí)賦值的,不過(guò)這個(gè)應(yīng)該也不重要,了解一下就好,應(yīng)該也沒(méi)有面試官會(huì)這么摳這個(gè)問(wèn)題吧。
本文鏈接:http://www.www897cc.com/showinfo-26-99899-0.htmlInheritableThreadLocal 是如何實(shí)現(xiàn)的父子線程局部變量的傳遞
聲明:本網(wǎng)頁(yè)內(nèi)容旨在傳播知識(shí),若有侵權(quán)等問(wèn)題請(qǐng)及時(shí)與本網(wǎng)聯(lián)系,我們將在第一時(shí)間刪除處理。郵件:2376512515@qq.com
上一篇: 利用依賴結(jié)構(gòu)矩陣管理架構(gòu)債務(wù)
下一篇: 軟件架構(gòu)中的九種耦合形式