自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

我把 ThreadLocal 能問的,都寫了

開發(fā) 前端
今天我們再來盤一盤 ThreadLocal ,這篇力求對(duì) ThreadLocal 一網(wǎng)打盡,徹底弄懂 ThreadLocal 的機(jī)制。

 [[419956]]

你好,我是yes。

今天我們再來盤一盤 ThreadLocal ,這篇力求對(duì) ThreadLocal 一網(wǎng)打盡,徹底弄懂 ThreadLocal 的機(jī)制。

有了這篇基礎(chǔ)之后,下篇再來盤一盤 ThreadLocal 的進(jìn)階版,等我哈。

話不多說,本文要解決的問題如下:

  • 為什么需要 ThreadLocal
  • 應(yīng)該如何設(shè)計(jì) ThreadLocal
  • 從源碼看ThreadLocal 的原理
  • ThreadLocal 內(nèi)存泄露之為什么要用弱引用
  • ThreadLocal 的最佳實(shí)踐
  • InheritableThreadLocal

好了,開車!

為什么需要 ThreadLocal

最近不是開放三胎政策嘛,假設(shè)你有三個(gè)孩子。

現(xiàn)在你帶著三個(gè)孩子出去逛街,路過了玩具店,三個(gè)孩子都看中了一款變形金剛。

所以你買了一個(gè)變形金剛,打算讓三個(gè)孩子輪著玩。

回到家你發(fā)現(xiàn),孩子因?yàn)檫@個(gè)玩具吵架了,三個(gè)都爭著要玩,誰也不讓著誰。

這時(shí)候怎么辦呢?你可以去拉架,去講道理,說服孩子輪流玩,但這很累。

所以一個(gè)簡單的辦法就是出去再買兩個(gè)變形金剛,這樣三個(gè)孩子都有各自的變形金剛,世界就暫時(shí)得到了安寧。

映射到我們今天的主題,變形金剛就是共享變量,孩子就是程序運(yùn)行的線程。

有多個(gè)線程(孩子),爭搶同一個(gè)共享變量(玩具),就會(huì)產(chǎn)生沖突,而程序的解決辦法是加鎖(父母說服,講道理,輪流玩),但加鎖就意味著性能的消耗(父母比較累)。

所以有一種解決辦法就是避免共享(讓每個(gè)孩子都各自擁有一個(gè)變形金剛),這樣線程之間就不需要競爭共享變量(孩子之間就不會(huì)爭搶)。

所以為什么需要 ThreadLocal?

就是為了通過本地化資源來避免共享,避免了多線程競爭導(dǎo)致的鎖等消耗。

這里需要強(qiáng)調(diào)一下,不是說任何東西都能直接通過避免共享來解決,因?yàn)橛行r(shí)候就必須共享。

舉個(gè)例子:當(dāng)利用多線程同時(shí)累加一個(gè)變量的時(shí)候,此時(shí)就必須共享,因?yàn)橐粋€(gè)線程的對(duì)變量的修改需要影響要另個(gè)線程,不然累加的結(jié)果就不對(duì)了。

再舉個(gè)不需要共享的例子:比如現(xiàn)在每個(gè)線程需要判斷當(dāng)前請(qǐng)求的用戶來進(jìn)行權(quán)限判斷,那這個(gè)用戶信息其實(shí)就不需要共享,因?yàn)槊總€(gè)線程只需要管自己當(dāng)前執(zhí)行操作的用戶信息,跟別的用戶不需要有交集。

好了,道理很簡單,這下子想必你已經(jīng)清晰了 ThreadLocal 出現(xiàn)的緣由了。

再來看一下 ThreadLocal 使用的小 demo。

  1. public class YesThreadLocal { 
  2.  
  3.     private static final ThreadLocal<String> threadLocalName = ThreadLocal.withInitial(() -> Thread.currentThread().getName()); 
  4.  
  5.     public static void main(String[] args) { 
  6.         for (int i = 0; i < 5; i++) { 
  7.             new Thread(() -> { 
  8.                 System.out.println("threadName: " + threadLocalName.get()); 
  9.             }, "yes-thread-" + i).start(); 
  10.         } 
  11.     } 

輸出結(jié)果如下:

可以看到,我在 new 線程的時(shí)候,設(shè)置了每個(gè)線程名,每個(gè)線程都操作同一個(gè) ThreadLocal 對(duì)象的 get 卻返回的各自的線程名,是不是很神奇?

應(yīng)該如何設(shè)計(jì) ThreadLocal ?

那應(yīng)該怎么設(shè)計(jì) ThreadLocal 來實(shí)現(xiàn)以上的操作,即本地化資源呢?

我們的目標(biāo)已經(jīng)明確了,就是用 ThreadLocal 變量來實(shí)現(xiàn)線程隔離。

從代碼上看,可能最直接的實(shí)現(xiàn)方法就是將 ThreadLocal 看做一個(gè) map ,然后每個(gè)線程是 key,這樣每個(gè)線程去調(diào)用 ThreadLocal.get 的時(shí)候,將自身作為 key 去 map 找,這樣就能獲取各自的值了。

聽起來很完美?錯(cuò)了!

這樣 ThreadLocal 就變成共享變量了,多個(gè)線程競爭 ThreadLocal ,那就得保證 ThreadLocal 的并發(fā)安全,那就得加鎖了,這樣繞了一圈就又回去了。

所以這個(gè)方案不行,那應(yīng)該怎么做?

答案其實(shí)上面已經(jīng)講了,是需要在每個(gè)線程的本地都存一份值,說白了就是每個(gè)線程需要有個(gè)變量,來存儲(chǔ)這些需要本地化資源的值,并且值有可能有多個(gè),所以怎么弄呢?

在線程對(duì)象內(nèi)部搞個(gè) map,把 ThreadLocal 對(duì)象自身作為 key,把它的值作為 map 的值。

這樣每個(gè)線程可以利用同一個(gè)對(duì)象作為 key ,去各自的 map 中找到對(duì)應(yīng)的值。

這不就完美了嘛!比如我現(xiàn)在有 3 個(gè) ThreadLocal 對(duì)象,2 個(gè)線程。

  1. ThreadLocal<String> threadLocal1 =  new ThreadLocal<>(); 
  2. ThreadLocal<Integer> threadLocal2 =  new ThreadLocal<>(); 
  3. ThreadLocal<Integer> threadLocal3 =  new ThreadLocal<>(); 

那此時(shí) ThreadLocal 對(duì)象和線程的關(guān)系如下圖所示:

這樣一來就滿足了本地化資源的需求,每個(gè)線程維護(hù)自己的變量,互不干擾,實(shí)現(xiàn)了變量的線程隔離,同時(shí)也滿足存儲(chǔ)多個(gè)本地變量的需求,完美!

JDK就是這樣實(shí)現(xiàn)的!我們來看看源碼。

從源碼看ThreadLocal 的原理

前面我們說到 Thread 對(duì)象里面會(huì)有個(gè) map,用來保存本地變量。

我們來看下 jdk 的 Thread 實(shí)現(xiàn)

  1. ThreadLocal<String> threadLocal1 =  new ThreadLocal<>(); 
  2. ThreadLocal<Integer> threadLocal2 =  new ThreadLocal<>(); 
  3. ThreadLocal<Integer> threadLocal3 =  new ThreadLocal<>(); 

可以看到,確實(shí)有個(gè) map ,不過這個(gè) map 是 ThreadLocal 的靜態(tài)內(nèi)部類,記住這個(gè)變量的名字 threadLocals,下面會(huì)有用的哈。

看到這里,想必有很多小伙伴會(huì)產(chǎn)生一個(gè)疑問。

竟然這個(gè) map 是放在 Thread 里面使用,那為什么要定義成 ThreadLocal 的靜態(tài)內(nèi)部類呢?

首先內(nèi)部類這個(gè)東西是編譯層面的概念,就像語法糖一樣,經(jīng)過編譯器之后其實(shí)內(nèi)部類會(huì)提升為外部頂級(jí)類,和平日里外部定義的類沒有區(qū)別,也就是說在 JVM 中是沒有內(nèi)部類這個(gè)概念的。

一般情況下非靜態(tài)內(nèi)部類用在內(nèi)部類,跟其他類無任何關(guān)聯(lián),專屬于這個(gè)外部類使用,并且也便于調(diào)用外部類的成員變量和方法,比較方便。

而靜態(tài)外部類其實(shí)就等于一個(gè)頂級(jí)類,可以獨(dú)立于外部類使用,所以更多的只是表明類結(jié)構(gòu)和命名空間。

所以說這樣定義的用意就是說明 ThreadLocalMap 是和 ThreadLocal 強(qiáng)相關(guān)的,專用于保存線程本地變量。

現(xiàn)在我們來看一下 ThreadLocalMap 的定義:

重點(diǎn)我已經(jīng)標(biāo)出來了,首先可以看到這個(gè) ThreadLocalMap 里面有個(gè) Entry 數(shù)組,熟悉 HashMap 的小伙伴可能有點(diǎn)感覺了。

這個(gè) Entry 繼承了 WeakReference 即弱引用。這里需要注意,不是說 Entry 自己是弱引用,看到我標(biāo)注的 Entry 構(gòu)造函數(shù)的 super(k) 沒,這個(gè) key 才是弱引用。

所以 ThreadLocalMap 里有個(gè) Entry 的數(shù)組,這個(gè) Entry 的 key 就是 ThreadLocal 對(duì)象,value 就是我們需要保存的值。

那是如何通過 key 在數(shù)組中找到 Entry 然后得到 value 的呢 ?

這就要從上面的 threadLocalName.get()說起,不記得這個(gè)代碼的滑上去看下示例,其實(shí)就是調(diào)用 ThreadLocal 的 get 方法。

此時(shí)就進(jìn)入 ThreadLocal#get方法中了,這里就可以得知為什么不同的線程對(duì)同一個(gè) ThreadLocal 對(duì)象調(diào)用 get 方法竟然能得到不同的值了。

這個(gè)中文注釋想必很清晰了吧!

ThreadLocal#get方法首先獲取當(dāng)前線程,然后得到當(dāng)前線程的 ThreadLocalMap 變量即 threadLocals,然后將自己作為 key 從 ThreadLocalMap 中找到 Entry ,最終返回 Entry 里面的 value 值。

這里我們再看一下 key 是如何從 ThreadLocalMap 中找到 Entry 的,即map.getEntry(this)是如何實(shí)現(xiàn)的,其實(shí)很簡單。

可以看到 ThreadLocalMap 雖然和 HashMap 一樣,都是基于數(shù)組實(shí)現(xiàn)的,但是它們對(duì)于 Hash 沖突的解決方法不一樣。

HashMap 是通過鏈表(紅黑樹)法來解決沖突,而 ThreadLocalMap 是通過開放尋址法來解決沖突。

聽起來好像很高級(jí),其實(shí)道理很簡單,我們來看一張圖就很清晰了。

所以說,如果通過 key 的哈希值得到的下標(biāo)無法直接命中,則會(huì)將下標(biāo) +1,即繼續(xù)往后遍歷數(shù)組查找 Entry ,直到找到或者返回 null。

可以看到,這種 hash 沖突的解決效率其實(shí)不高,但是一般 ThreadLocal 也不會(huì)太多,所以用這種簡單的辦法解決即可。

至于代碼中的expungeStaleEntry我們等下再分析,先來看下 ThreadLocalMap#set 方法,看看寫入的怎樣實(shí)現(xiàn)的,來看看 hash 沖突的解決方法是否和上面說的一致。

可以看到 set 的邏輯也很清晰。

先通過 key 的 hash 值計(jì)算出一個(gè)數(shù)組下標(biāo),然后看看這個(gè)下標(biāo)是否被占用了,如果被占了看看是否就是要找的 Entry 。

如果是則進(jìn)行更新,如果不是則下標(biāo)++,即往后遍歷數(shù)組,查找下一個(gè)位置,找到空位就 new 個(gè) Entry 然后把坑給占用了。

當(dāng)然,這種數(shù)組操作一般免不了閾值的判斷,如果超過閾值則需要進(jìn)行擴(kuò)容。

上面的清理操作和 key 為空的情況,下面再做分析,這里先略過。

至此,我們已經(jīng)分析了 ThreadLocalMap 的核心操作 get 和 set ,想必你對(duì) ThreadLocalMap 的原理已經(jīng)從源碼層面清晰了!

可能有些小伙伴對(duì) key 的哈希值的來源有點(diǎn)疑惑,所以我再來補(bǔ)充一下 key.threadLocalHashCode的分析。

可以看到key.threadLocalHashCode其實(shí)就是調(diào)用 nextHashCode 進(jìn)行一個(gè)原子類的累加。

注意看上面都是靜態(tài)變量和靜態(tài)方法,所以在 ThreadLocal 對(duì)象之間是共享的,然后通過固定累加一個(gè)奇怪的數(shù)字0x61c88647來分配 hash 值。

這個(gè)數(shù)字當(dāng)然不是亂寫的,是實(shí)驗(yàn)證明的一個(gè)值,即通過 0x61c88647 累加生成的值與 2 的冪取模的結(jié)果,可以較為均勻地分布在 2 的冪長度的數(shù)組中,這樣可以減少 hash 沖突。

有興趣的小伙伴可以深入研究一下,反正我沒啥興趣。

ThreadLocal 內(nèi)存泄露之為什么要用弱引用

接下來就是要解決上面挖的坑了,即 key 的弱引用、Entry 的 key 為什么可能為 null、還有清理 Entry 的操作。

之前提到過,Entry 對(duì) key 是弱引用,那為什么要弱引用呢?

我們知道,如果一個(gè)對(duì)象沒有強(qiáng)引用,只有弱引用的話,這個(gè)對(duì)象是活不過一次 GC 的,所以這樣的設(shè)計(jì)就是為了讓當(dāng)外部沒有對(duì) ThreadLocal 對(duì)象有強(qiáng)引用的時(shí)候,可以將 ThreadLocal 對(duì)象給清理掉。

那為什么要這樣設(shè)計(jì)呢?

假設(shè) Entry 對(duì) key 的引用是強(qiáng)引用,那么來看一下這個(gè)引用鏈:

從這條引用鏈可以得知,如果線程一直在,那么相關(guān)的 ThreadLocal 對(duì)象肯定會(huì)一直在,因?yàn)樗恢北粡?qiáng)引用著。

看到這里,可能有人會(huì)說那線程被回收之后就好了呀。

重點(diǎn)來了!線程在我們應(yīng)用中,常常是以線程池的方式來使用的,比如 Tomcat 的線程池處理了一堆請(qǐng)求,而線程池中的線程一般是不會(huì)被清理掉的,所以這個(gè)引用鏈就會(huì)一直在,那么 ThreadLocal 對(duì)象即使沒有用了,也會(huì)隨著線程的存在,而一直存在著!

所以這條引用鏈需要弱化一下,而能操作的只有 Entry 和 key 之間的引用,所以它們之間用弱引用來實(shí)現(xiàn)。

與之對(duì)應(yīng)的還有一個(gè)條引用鏈,我結(jié)合著上面的線程引用鏈都畫出來:

另一條引用鏈就是棧上的 ThreadLocal 引用指向堆中的 ThreadLocal 對(duì)象,這個(gè)引用是強(qiáng)引用。

如果有這條強(qiáng)引用存在,那說明此時(shí)的 ThreadLocal 是有用的,此時(shí)如果發(fā)生 GC 則 ThreadLocal 對(duì)象不會(huì)被清除,因?yàn)橛袀€(gè)強(qiáng)引用存在。

當(dāng)隨著方法的執(zhí)行完畢,相應(yīng)的棧幀也出棧了,此時(shí)這條強(qiáng)引用鏈就沒了,如果沒有別的棧有對(duì) ThreadLocal 對(duì)象的引用,那么說明 ThreadLocal 對(duì)象無法再被訪問到(定義成靜態(tài)變量的另說)。

那此時(shí) ThreadLocal 只存在與 Entry 之間的弱引用,那此時(shí)發(fā)生 GC 它就可以被清除了,因?yàn)樗鼰o法被外部使用了,那就等于沒用了,是個(gè)垃圾,應(yīng)該被處理來節(jié)省空間。

至此,想必你已經(jīng)明白為什么 Entry 和 key 之間要設(shè)計(jì)為弱引用,就是因?yàn)槠饺站€程的使用方式基本上都是線程池,所以線程的生命周期就很長,可能從你部署上線后一直存在,而 ThreadLocal 對(duì)象的生命周期可能沒這么長。

所以為了能讓已經(jīng)沒用 ThreadLocal 對(duì)象得以回收,所以 Entry 和 key 要設(shè)計(jì)成弱引用,不然 Entry 和 key是強(qiáng)引用的話,ThreadLocal 對(duì)象就會(huì)一直在內(nèi)存中存在。

但是這樣設(shè)計(jì)就可能產(chǎn)生內(nèi)存泄漏。

那什么叫內(nèi)存泄漏?

就是指:程序中已經(jīng)無用的內(nèi)存無法被釋放,造成系統(tǒng)內(nèi)存的浪費(fèi)。

當(dāng) Entry 中的 key 即 ThreadLocal 對(duì)象被回收了之后,會(huì)發(fā)生 Entry 中 key 為 null 的情況,其實(shí)這個(gè) Entry 就已經(jīng)沒用了,但是又無法被回收,因?yàn)橛?Thread->ThreadLocalMap ->Entry 這條強(qiáng)引用在,這樣沒用的內(nèi)存無法被回收就是內(nèi)存泄露。

那既然會(huì)有內(nèi)存泄漏還這樣實(shí)現(xiàn)?

這里就要填一填上面的坑了,也就是涉及到的關(guān)于 expungeStaleEntry即清理過期的 Entry 的操作。

設(shè)計(jì)者當(dāng)然知道會(huì)出現(xiàn)這種情況,所以在多個(gè)地方都做了清理無用 Entry ,即 key 已經(jīng)被回收的 Entry 的操作。

比如通過 key 查找 Entry 的時(shí)候,如果下標(biāo)無法直接命中,那么就會(huì)向后遍歷數(shù)組,此時(shí)遇到 key 為 null 的 Entry 就會(huì)清理掉,再貼一下這個(gè)方法:

這個(gè)方法也很簡單,我們來看一下它的實(shí)現(xiàn):

所以在查找 Entry 的時(shí)候,就會(huì)順道清理無用的 Entry ,這樣就能防止一部分的內(nèi)存泄露啦!

還有像擴(kuò)容的時(shí)候也會(huì)清理無用的 Entry:

其它還有,我就不貼了,反正知曉設(shè)計(jì)者是做了一些操作來回收無用的 Entry 的即可。

ThreadLocal 的最佳實(shí)踐

當(dāng)然,等著這些操作被動(dòng)回收不是最好的方法,假設(shè)后面沒人調(diào)用 get 或者調(diào)用 get 都直接命中或者不會(huì)發(fā)生擴(kuò)容,那無用的 Entry 豈不是一直存在了嗎?所以上面說只能防止一部分的內(nèi)存泄露。

所以,最佳實(shí)踐是用完了之后,調(diào)用一下 remove 方法,手工把 Entry 清理掉,這樣就不會(huì)發(fā)生內(nèi)存泄漏了!

  1. void yesDosth { 
  2.  threadlocal.set(xxx); 
  3.  try { 
  4.   // do sth 
  5.  } finally { 
  6.   threadlocal.remove(); 
  7.  } 

這就是使用 Threadlocal 的一個(gè)正確姿勢啦,即不需要的時(shí)候,顯示的 remove 掉。

當(dāng)然,如果不是線程池使用方式的話,其實(shí)不用關(guān)系內(nèi)存泄漏,反正線程執(zhí)行完了就都回收了,但是一般我們都是使用線程池的,可能只是你沒感覺到。

比如你用了 tomcat ,其實(shí)請(qǐng)求的執(zhí)行用的就是 tomcat 的線程池,這就是隱式使用。

還有一個(gè)問題,關(guān)于 withInitial 也就是初始化值的方法。

由于類似 tomcat 這種隱式線程池的存在,即線程第一次調(diào)用執(zhí)行 Threadlocal 之后,如果沒有顯示調(diào)用 remove 方法,則這個(gè) Entry 還是存在的,那么下次這個(gè)線程再執(zhí)行任務(wù)的時(shí)候,不會(huì)再調(diào)用 withInitial 方法,也就是說會(huì)拿到上一次執(zhí)行的值。

但是你以為執(zhí)行任務(wù)的是新線程,會(huì)初始化值,然而它是線程池里面的老線程,這就和預(yù)期不一致了,所以這里需要注意。

InheritableThreadLocal

這個(gè)其實(shí)之前文章寫過了,不過這次竟然寫了 threadlocal 就再拿出來。

這玩意可以理解為就是可以把父線程的 threadlocal 傳遞給子線程,所以如果要這樣傳遞就用 InheritableThreadLocal ,不要用 threadlocal。

原理其實(shí)很簡單,在 Thread 中已經(jīng)包含了這個(gè)成員:

在父線程創(chuàng)建子線程的時(shí)候,子線程的構(gòu)造函數(shù)可以得到父線程,然后判斷下父線程的 InheritableThreadLocal 是否有值,如果有的話就拷過來。

這里要注意,只會(huì)在線程創(chuàng)建的時(shí)會(huì)拷貝 InheritableThreadLocal 的值,之后父線程如何更改,子線程都不會(huì)受其影響。

最后

至此有關(guān) ThreadLocal 的知識(shí)點(diǎn)就差不多了。

想必你已經(jīng)清楚 ThreadLocal 的原理,包括如何實(shí)現(xiàn),為什么 key 要設(shè)計(jì)成弱引用,并且關(guān)于在線程池中使用的注意點(diǎn)等等。

其實(shí)本沒打算寫 ThreadLocal 的,因?yàn)樽罱诳?Netty ,所以想寫一下 FastThreadLocal ,但是前置知識(shí)點(diǎn)是 ThreadLocal ,所以就干了這篇。

消化了這篇之后,出去面試 ThreadLocal 算是沒問題了吧,最后再留個(gè)小小的思考題。

那為什么 Entry 中的 value 不弱引用?

這個(gè)題目來自群友的一個(gè)面試題哈,想必看完這篇文章之后,這個(gè)題目難不倒你,歡迎留言區(qū)寫出答案!

等我下篇的 ThreadLocal 進(jìn)階版!

責(zé)任編輯:武曉燕 來源: yes的練級(jí)攻略
相關(guān)推薦

2020-07-27 08:13:03

RabbitMQ代碼系統(tǒng)

2021-01-19 05:24:36

ThreadLocal線程編程

2022-11-04 08:47:52

底層算法數(shù)據(jù)

2022-05-14 21:19:22

ThreadLocaJDKsynchroniz

2020-07-28 08:59:22

JavahreadLocal面試

2023-11-02 08:27:29

2021-01-26 05:07:53

WindowViewWMS

2023-03-27 08:03:46

ChatGPTMidjourney主角

2020-09-11 06:39:29

ThreadLocal線程

2024-03-13 07:53:57

弱引用線程工具

2022-12-08 17:12:34

注解源碼場景

2024-02-06 07:56:20

數(shù)據(jù)庫分布式數(shù)據(jù)庫架構(gòu)產(chǎn)品

2022-04-01 12:40:13

MySQL數(shù)據(jù)庫

2022-01-24 14:08:16

Redis面試命令

2015-11-16 14:52:13

代碼程序員

2019-11-20 10:38:36

路由路由協(xié)議路由器

2022-04-01 08:37:07

SpringAPI前端

2023-11-03 08:10:49

ThreadLoca內(nèi)存泄露

2020-05-15 09:30:12

代碼函數(shù)語言

2020-11-20 14:45:48

HandlerAndroid代碼
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)