保姆級教學,22張圖揭開ThreadLocal

前言

圖解方式來通關ThreadLocal,同時但願大家有必定的JVM 基礎,這樣食用起來會更香。程序員

相信大夥對 ThreadLocal 並不陌生,工做中經常使用,同時也是面試高頻題,可是大部分人對 ThreadLocal 的理解可能只是「線程的本地變量,Map結構」,看完本文讓大夥真正理解ThreadLocal,給大夥工做帶來幫助,也讓面試有更多的談資。面試

內容大綱

Java對象引用級別

在聊 ThreadLocal 前,先作前置知識鋪墊,談談Java對象引用級別。數據庫

爲了使程序能更靈活地控制對象生命週期,從 JDK1.2 版本開始,JDK把對象的引用級別由高到低分爲強引用、軟引用、弱引用、虛引用四種級別。數組

強引用 StrongReference

強引用是咱們最多見的對象,它屬於不可回收資源,垃圾回收器(後面簡稱G C)絕對不會回收它,即便是內存不足,J V M寧願拋出 OutOfMemoryErrorM 異常,使程序終止,也不會來回收強引用對象。緩存

軟引用 SoftReference

若是對象是軟引用,那它的性質屬於無關緊要,由於內存空間充足的狀況下,G C不會回收它,可是內存空間緊張,G C發現它僅有軟引用,就會回收該對象,因此軟引用對象適合做爲內存敏感的緩存對象。安全

只有對象僅被 SoftReference 引用,它纔是軟引用級別對象,由於對象能夠在多處被引用,因此 SoftReference 引用的對象,它可能在其餘處被強引用了。網絡

弱引用 WeakReference

弱引用對象相對軟引用對象具備更短暫的生命週期,只要 G C 發現它僅有弱引用,無論內存空間是否充足,都會回收它,不過 G C 是一個優先級很低的線程,所以不必定會很快發現那些僅有弱引用的對象。框架

只有對象僅被 WeakReference 引用,它纔是弱引用級別對象,由於對象能夠在多處被引用,因此 WeakReference 引用的對象,它可能在其餘處被強引用了。分佈式

虛引用 PhantomReference

顧名思義,虛引用形同虛設,與其餘幾種引用不一樣,虛引用不會決定對象的生命週期。工具

若是一個對象僅有虛引用,那它就和沒有任何引用同樣,任什麼時候候均可能被 G C 回收。

讀到這裏會不會感受虛引用和弱引用沒區別?它們的區別以下

  • SoftReference、WeakReference引用的對象沒被回收時,可使用get方法獲取真實對象地址
  • PhantomReference使用get方法永遠返回null

簡單說就是「沒法經過虛引用來獲取對象的真實地址」

小結

Java中SoftReference、WeakReference、PhantomReference,能夠理解爲對象引用級別包裝類,在項目中使用對應的包裝類,賦予對象引用級別。

虛引用圖中,出現了ReferenceQueue(引用隊列),引用隊列是配合對象引用級別包裝類(SoftReference、WeakReference、PhantomReference)使用,當對象引用級別包裝類所指向的對象,被垃圾回收後,該對象引用級別包裝類被追加到引用隊列,所以能夠經過引用隊列作 G C 相關統計或額外數據清理等操做。

ThreadLocal

ThreadLocal不少地方叫線程本地變量,也有些地方叫線程本地存儲,其實意思差很少。ThreadLocal爲變量在每一個線程中都建立了一個副本,每一個線程能夠訪問本身內部的副本變量。

ThreadLocal是什麼

Thread類聲明瞭成員變量threadLocals,threadLocals纔是真正的線程本地變量,所以每一個 Thread 都有本身的線程本地變量,因此線程本地變量擁有線程隔離特性,也就是天生的線程安全。

從上圖能夠看到 threadLocals 成員變量類是 ThreadLocal.ThreadLocalMap,便是 ThreadLocal 提供的內部類,所以 Thread 線程本地變量的建立、新增、獲取、刪除實現核心,必然是圍繞 threadLocals,因此開發者也是圍繞 threadLocals 實現功能,爲了後續重複使用,還會對代碼實現進行封裝複用,而 ThreadLocal 就是線程本地變量工具類,由 J D K 提供,線程本地變量的功能都已經實現好了,開箱即用,造福廣大開發人員。

ThreadLocal經常使用的方法

  • set:爲當前線程設置變量,當前ThreadLocal做爲索引
  • get:獲取當前線程變量,當前ThreadLocal做爲索引
  • initialValue(鉤子方法須要子類實現):賴加載形式初始化線程本地變量,執行get時,發現線程本地變量爲null,就會執行initialValue的內容
  • remove:清空當前線程的ThreadLocal索引與映射的元素

一個 Threa能夠擁有多個 ThreadLocal鍵值對(存儲在ThreadLocalMap結構),又由於 ThreadLocalMap 依賴當前Thread,Thread銷燬時 ThreadLocalMap 也會隨之銷燬,因此 ThreadLocalMap 的生命週期與 Thread 綁定。

如今總結出「本地線程變量的做用域,屬於當前線程整個範圍,一個線程能夠跨越多個方法使用本地線程變量」,當你但願某些變量在某 Thread 的多個方法中共享 並保證線程安全,那就大膽的使用ThreadLocal(ps:必定要想清楚,是某個變量被Thread生命週期內多個方法共享,仍是多個Thread共享這個變量!)。

ThreadLocal源碼

先來看看User類實現的線程本地變量代碼

方法也很少,分別是initialValue、get、set、remove,接下來這些方法源碼進行解析。

ThreadLocalMap結構

爲了後面的源碼解析體驗更好,有必要介紹下ThreadLocalMap,顧名思義,它是 Map 結構,可是本文主要內容不是Map,因此上一圖,快速過一下這塊內容。

經過上圖,相信大夥對 ThreadLocalMap 結構已經很是清晰,不知有沒有細心的小夥伴發現 ThreadLocal 竟被弱引用持有?

爲何ThreadLocal會被弱引用?這塊疑惑後面會給大夥安排的明明白白,最後上一張 ThreadLocalMap 源碼圖。

get 獲取變量

步驟以下

  1. 獲取當前線程
  2. 獲取當前線程的本地變量
  3. 線程本地變量沒有被建立,執行setInitialValue方法進行初始化,並返回value值
  4. 線程本地變量存在,ThreadLocal計算成索引從 本地線程變量 獲取Entry,若是Entry爲null,執行setInitialValue方法進行初始化,並返回value值,不然經過Entry獲取value返回

initialValue方法

步驟以下

  1. 經過get方法觸發
  2. 執行初始化,獲取到value
  3. 獲取當前線程
  4. 獲取當前線程本地變量
  5. 若是當前線程本地變量存在 ,ThreadLocal計算成索引設置映射的value,不然建立線程本地變量再作後續的設置操做
  6. 返回value值

set 設置變量

步驟以下

  1. 獲取當前線程
  2. 獲取線程本地變量
  3. 本地變量不爲空,當前ThreadLocal爲索引設置映射的value,不然建立線程本地變量再作後續的設置操做

remove 清除變量

步驟以下

  1. 獲取Entry數組
  2. 當前ThreadLocal計算出索引
  3. 根據索引獲取Entry元素(如果第一次沒有命中,就循環直到null)
  4. 清除Entry元素

小結

源碼十分簡單,核心就三樣ThreadLocal線程本地變量工具類(同時做爲索引)、Entry基本元素(由弱引用包裝類ThreadLocal與value組成),Entry數組容器,到這裏流程很清晰了,ThreadLocal計算出數組索引,用 ThreadLocal 與 value 構建出 Entry 元素,最終放入 Entry 容器中,相信大夥都能寫出來。

爲什麼採用弱引用

爲何 Entry 中對 ThreadLocal 使用弱引用?反問一句,若是使用強引用,會發生什麼事情?

上圖的代碼做用僅僅只是是爲了讓大夥去理解爲何使用弱引用,通常開發中不會出現這樣的代碼(真出現了,這程序員怕是要拉去祭天)。

回到正題,咱們快速對代碼進行解析,首先 ThreadContextTest 持有私有的靜態變量 ThreadLocal,且 ThreadContextTest 禁止實例化,接着執行靜態方法 run 觸發靜態塊爲 ThreadLocal 設置User變量 並消除 ThreadLocal 強引用,此時當前線程的本地變量擁有了Entry元素。

問題來了,要如何獲取到 Entry 元素,按正常流程,ThreadLocal執行 get 方法,get會使用當前 ThreadLocal 計算出索引,最終獲取到Entry元素,但是如今的問題如同下圖。

咱們不知道 key 是什麼,如何去獲取映射的value,一樣的道理,都沒有入口去獲取到ThreadContextTest.ThreadLoca,天然沒辦法獲取映射的Entry元素。

設計中採用Map結構存儲數據,卻不能經過key去獲取value,這設計明顯不合理,又因key、value值是強引用,致使 G C 沒法回收,形成內存溢出。

因此針對這種不合理的設計場景 J D K 作了優化,對 Entry 中的 ThreadLocal 使用弱引用,當 G C 發現它僅有弱引用的時候,會進行回收。

remove背後的意義

還沒結束,上面留了個小尾巴,大夥都知道 Entry 中對 ThreadLocal 使用弱引用,但value是強引用,若是出現上面提到的不合理場景,value值沒法清理,最終內存溢出。

其實value做爲強引用設計屬於合理,若是用軟或弱引用,就出大問題了,程序跑着跑着忽然get到了一個null,估計都得罵娘了,因此爲解決內存溢出問題 J D K提供remove方法,使開發人員能夠選擇手動清理整個Entry元素,防止內存溢出。

還記的以前說過嗎?線程本地變量的生命週期與線程綁定,通常線程的生命週期比較短,線程結束時,線程本地變量天然就銷燬了,軟引用與 remove 會不會有點多餘了?

業務瞬息萬變,大部分狀況來講線程的生命週期比較短,但也業務場景會致使線程的生命週期較長,甚至可能線程無限循環執行,這些是你沒辦法預料到的,數量一旦上來很容易內存溢出,因此我的建議使用完以後及時清理ThreadLocal,理由以下

  • 生命週期較長的線程場景
  • 無限循環線程的場景
  • 線程池場景(由於線程池能夠複用線程,並且公司使用的框架可能會定製化線程池,你不能保證他會在線程池內幫你remove)

嘮叨嘮叨

先祝大夥新年快樂,萬事如意!!!博主兩週肝一篇,雖然週期有點長,可是質量有保證,碼文不易,若是以爲本文對您有幫助,歡迎分享給你的朋友,也給阿星點個「點贊+收藏」,這對阿星很是重要,謝謝您們,給各位小姐姐小哥哥們抱拳了,咱們下次見!

關於我

公衆號 : 「程序猿阿星」 專一技術原理、源碼,經過圖解方式輸出技術,這裏將會分享操做系統、計算機網絡、Java、分佈式、數據庫等精品原創文章,期待你的關注。

相關文章
相關標籤/搜索