日韩无码专区无码一级三级片|91人人爱网站中日韩无码电影|厨房大战丰满熟妇|AV高清无码在线免费观看|另类AV日韩少妇熟女|中文日本大黄一级黄色片|色情在线视频免费|亚洲成人特黄a片|黄片wwwav色图欧美|欧亚乱色一区二区三区

RELATEED CONSULTING
相關(guān)咨詢
選擇下列產(chǎn)品馬上在線溝通
服務(wù)時間:8:30-17:00
你可能遇到了下面的問題
關(guān)閉右側(cè)工具欄

新聞中心

這里有您想知道的互聯(lián)網(wǎng)營銷解決方案
我把ThreadLocal能問的,都寫了

你好,我是yes。

為灣里等地區(qū)用戶提供了全套網(wǎng)頁設(shè)計(jì)制作服務(wù),及灣里網(wǎng)站建設(shè)行業(yè)解決方案。主營業(yè)務(wù)為成都網(wǎng)站設(shè)計(jì)、網(wǎng)站制作、灣里網(wǎng)站設(shè)計(jì),以傳統(tǒng)方式定制建設(shè)網(wǎng)站,并提供域名空間備案等一條龍服務(wù),秉承以專業(yè)、用心的態(tài)度為用戶提供真誠的服務(wù)。我們深信只要達(dá)到每一位用戶的要求,就會得到認(rèn)可,從而選擇與我們長期合作。這樣,我們也可以走得更遠(yuǎn)!

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

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

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

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

好了,開車!

為什么需要 ThreadLocal

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

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

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

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

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

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

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

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

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

所以為什么需要 ThreadLocal?

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

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

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

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

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

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

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

輸出結(jié)果如下:

可以看到,我在 new 線程的時候,設(shè)置了每個線程名,每個線程都操作同一個 ThreadLocal 對象的 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 看做一個 map ,然后每個線程是 key,這樣每個線程去調(diào)用 ThreadLocal.get 的時候,將自身作為 key 去 map 找,這樣就能獲取各自的值了。

聽起來很完美?錯了!

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

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

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

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

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

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

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

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

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

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

從源碼看ThreadLocal 的原理

前面我們說到 Thread 對象里面會有個 map,用來保存本地變量。

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

這個中文注釋想必很清晰了吧!

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)的,但是它們對于 Hash 沖突的解決方法不一樣。

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

所以為了能讓已經(jīng)沒用 ThreadLocal 對象得以回收,所以 Entry 和 key 要設(shè)計(jì)成弱引用,不然 Entry 和 key是強(qiáng)引用的話,ThreadLocal 對象就會一直在內(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 對象被回收了之后,會發(fā)生 Entry 中 key 為 null 的情況,其實(shí)這個 Entry 就已經(jīng)沒用了,但是又無法被回收,因?yàn)橛?Thread->ThreadLocalMap ->Entry 這條強(qiáng)引用在,這樣沒用的內(nèi)存無法被回收就是內(nèi)存泄露。

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

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

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

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

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

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

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

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

ThreadLocal 的最佳實(shí)踐

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

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

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

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

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

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

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

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

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

InheritableThreadLocal

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

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

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

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

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

最后

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

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

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

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

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

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

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


當(dāng)前文章:我把ThreadLocal能問的,都寫了
文章鏈接:http://www.5511xx.com/article/djgdoip.html