嘮點(diǎn)面試官愛聽的系列之ThreadLocal
面試官:“看你簡(jiǎn)歷寫了熟悉 Java 并發(fā)編程,那你給我講講 ThreadLocal 吧?!?/p>
我:“ThreadLocal 是 Java 中的一個(gè)類,用于創(chuàng)建線程局部變量。每個(gè)線程對(duì) ThreadLocal 變量的訪問都是獨(dú)立的,每個(gè)線程都會(huì)擁有自己獨(dú)立的副本?!?/p>
完蛋,這么嘮,面試官一看就是背的八股,一點(diǎn)自己的東西都沒有。
你得嘮點(diǎn)面試官愛聽的,要有一定深度,讓面試官看出自己的思考。
下面我先來打個(gè)樣。
我在實(shí)際開發(fā)項(xiàng)目有利用過 ThreadLocal 存儲(chǔ)用戶信息,并對(duì) ThreadLocal 源碼有過一定研究。
ThreadLocal 的優(yōu)勢(shì)是無鎖化提升并發(fā)性能和簡(jiǎn)化變量的傳遞邏輯,每個(gè)線程對(duì) ThreadLocal 變量的訪問都是獨(dú)立的,每個(gè)線程都會(huì)擁有自己獨(dú)立的副本。
我:“需要我展開聊聊嗎?”
面試官:“好的?!?/p>
ThreadLocal 存儲(chǔ)的變量實(shí)際上是存儲(chǔ)在 Thread 線程對(duì)象中,在 Thread 類中有兩個(gè) ThreadLocalMap 類型的變量一個(gè)是 threadLocals,另一個(gè)是 inheritableThreadLocals。
其中 threadLocals 就是用于存儲(chǔ) ThreadLocal 對(duì)應(yīng)的變量。ThreadLocalMap 也是哈希數(shù)據(jù)結(jié)構(gòu),不過與我們用的最多的 HashMap 有所不同。
ThreadLocalMap 解決哈希沖突采用的是線性探測(cè)法,而 HashMap 采用的是拉鏈法。
ThreadLocalMap 的初始容量是 16,當(dāng)負(fù)載達(dá)到 2/3 的時(shí)候會(huì)觸發(fā)擴(kuò)容邏輯,擴(kuò)容的時(shí)候容量*2。
ThreadLocalMap 通過 Entry 數(shù)組存儲(chǔ)數(shù)據(jù)。每一個(gè) Entry 對(duì)象的 key 是一個(gè)弱引用指向的 ThreadLocal 對(duì)象。值是一個(gè)強(qiáng)引用的對(duì)象,類型由 ThreadLocal 對(duì)象的泛型 <> 決定。
在 Entry 對(duì)象中 ThreadLocal 之所以使用弱引用進(jìn)行鏈接是為了減少當(dāng)內(nèi)存泄露發(fā)生時(shí)所帶來的內(nèi)存損失。
一旦 ThreadLocal 對(duì)象失去了外界強(qiáng)引用,在發(fā)生垃圾回收時(shí)僅被 Entry 對(duì)象弱引用的 ThreadLocal 對(duì)象就會(huì)被垃圾回收器回收,這時(shí)該 Entry 對(duì)象就是所謂的過時(shí) Entry。
過時(shí) Entry 自身及其引用的 vlaue 值在其它 ThreadLocal 對(duì)象執(zhí)行 get、set、remove 方法時(shí),可能會(huì)被清理,從而釋放泄露的內(nèi)存。
當(dāng)然在實(shí)際開發(fā)中,我們更應(yīng)該主動(dòng)在恰當(dāng)時(shí)機(jī)調(diào)用 remove 方法,對(duì)不再使用的 ThreadLocal 對(duì)象進(jìn)行清理,避免觸發(fā) ThreadLocal 的清理機(jī)制,進(jìn)而提升 get、set、remove 方法的執(zhí)行效率。
需要注意的一點(diǎn),我們通常所使用的 web 容器 tomcat 對(duì)工作線程的管理使用了池化技術(shù),也就是說 Thread 對(duì)象會(huì)被重復(fù)使用。
如果我們?cè)谝粋€(gè)請(qǐng)求結(jié)束的時(shí)候沒有調(diào)用 remove 方法清理 ThreadLocal 對(duì)象,并且其他請(qǐng)求在執(zhí)行 get 方法前沒有執(zhí)行 set 方法進(jìn)行設(shè)置值,那么可能會(huì)發(fā)生匪夷所思的業(yè)務(wù)異常。
咦?為什么當(dāng)前是 A 發(fā)起的請(qǐng)求,獲取到的卻是 B 的信息?
圖片
ThreadLocal 類有一個(gè)子類叫做 InheritableThreadLocal,InheritableThreadLocal 主要解決在單次請(qǐng)求過程中涉及到了多線程異步處理邏輯時(shí),ThreadLocal 變量無法傳遞問題。
雙十一有一個(gè)比價(jià)需求,需要拉取同商品在拼多多、淘寶、京東、抖音上的價(jià)格進(jìn)行比價(jià),如果采用單線程去執(zhí)行,需要依次進(jìn)行調(diào)用,該接口總耗時(shí)為獲取各大平臺(tái)價(jià)格耗時(shí)的累加和。
此時(shí)要想提高接口執(zhí)行效率可以采取多線程方案,但是在獲取各大平臺(tái)優(yōu)惠價(jià)時(shí)需要獲取 ThreadLocal 中存儲(chǔ)的上下文信息。
但是,我們前面說過 ThreadLocal 儲(chǔ)存的上下文,其實(shí)是存儲(chǔ)在了 Thread 對(duì)象中。
而采取多線程方案時(shí),獲取各大平臺(tái)價(jià)格的線程并不是之前的主線程,這時(shí)是無法直接通過 ThreadLocal 的 get 方法獲取到在主線程存儲(chǔ)的上下文信息。
此時(shí)怎么辦呢?有兩種方案,第一種是在開啟多線程之前先取出上下文信息,然后作為參數(shù)傳遞給每一個(gè)線程,但是這不是與 ThreadLocal 簡(jiǎn)化變量的傳遞邏輯的初衷相悖了嗎?
通過 InheritableThreadLocal 就可以很好的解決這個(gè)問題,在線程初始化的時(shí)候,當(dāng)前線程的 inheritableThreadLocals 會(huì)拷貝給新創(chuàng)建線程的 inheritableThreadLocals。
inheritableThreadLocals 就是上文提到的 Thread 線程對(duì)象中的另外一個(gè) ThreadLocalMap 類型變量,用于存儲(chǔ) InheritableThreadLocal 記錄的信息。
圖片
我:“以上就是我對(duì) ThreadLocal 的一些認(rèn)識(shí)?!?/p>
內(nèi)心甚至期盼著面試官繼續(xù)深究,看過源碼解讀系列完全不慌!
想要進(jìn)行一步了解 ThreadLocal 的小伙伴可以回看 ThreadLocal 源碼解讀系列。