45張圖搞定!ThreadLocal的最牛辨析

與Synchonized的比較,它的作用是什麼

ThreadLocal和Synchonized都用於解決多線程併發訪問。可是ThreadLocal與Synchronized有着本質的區別。Synchronized是利用鎖的機制,使變量或代碼代碼塊在某一個時刻僅僅能被一個線程訪問。

從名字我們就可以看到ThreadLocal叫做線程變量,意思是ThreadLocal中填充的變量屬於當前線程,該變量對其他線程而言是隔離的。ThreadLocal爲變量在每個線程中都創建了一個副本,那麼每個線程可以訪問自己內部的副本變量。

從字面意思非常容易理解,但是從實際使用的角度來看就沒那麼容易了。作爲一個面試常問的點,使用場景那也是相當的豐富。

  1. 在進行對象跨層次傳遞的時候,使用ThreadLocal可以避免多次傳遞,打破層次間的束縛。
  2. 線程間層次隔離。
  3. 進行事務操作,用於存儲線程事務信息。
  4. 數據庫連接,Session會話管理。

現在應該對ThreadLocal已經有一個大概的認識了。下面看看具體如何使用。

ThreadLocal怎麼使用

既然ThreadLocal的作用是每一個線程創建一個副本,那我們使用一個例子來驗證一下:

從結果可知,每一個線程都有各自的local值。也就是說,threadLocal的值是線程與線程分離的。具體原理可以畫出以下不同線程中ThreadLocalMap是如何存儲數據的。

如果是第一次學習ThreadLocal的朋友可能看懵了,ThreadLocal我都沒看懂,你跟我說ThreadLocalMap?別急,我們接着往下看。

這裏整理了最近BAT最新面試題,2020船新版本!!需要的朋友可以點擊:這個,點這個!!,備註:簡書。希望那些有需要朋友能在今年第一波招聘潮找到一個自己滿意順心的工作!

ThreadLocal的使用場景—數據庫連接

我們知道,數據庫連接池最爲我們詬病的就是連接的創建與關閉。這其中要耗費大量的資源與時間。我們的ThreadLocal也可以幫我們解決這個問題。

這是一個數據庫連接的管理類。我們在使用數據庫的時候首先就是建立數據庫連接。然後用完了之後就關閉。這樣做有一個很嚴重的問題,如果有1個用戶頻繁使用數據庫,那麼就需要建立多次連接和關閉。這樣我們服務器可能喫不消,那麼怎麼辦呢?如果一萬個客戶端,那麼服務器壓力更大。

這時最好使用ThreadLocal。因爲ThreadLocal在每個線程中會創建一個副本。並且在線程內部任何地方可以使用。線程之間互不影響。這樣一來就不存在線程安全問題,也不會嚴重影響程序執行性能,避免了connection的頻繁創建和銷燬。(當然實際中我們有數據庫連接池可以處理,但我們的目的都很明確,避免連接對象的頻繁創建與銷燬!)

以上主要講解了一個基本的案例,然後還分析了爲什麼在數據庫連接的時候會使用ThreadLocal。下面我們從源碼的角度分析ThreadLocal的工作原理。

ThreadLocal源碼分析

ThreadLocal類接口簡介

ThreadLocal類接口很簡單,只有4個方法,先來了解一下:

1. void set(Object value);//設置當前線程的線程局部變量值

2. public Object get();//該方法返回當前線程所對應的線程局部變量

3. public void remove();//當線程局部變量的值刪除,目的是爲了減少內存的佔用。該方法是JDK5.0新增的方法,需要指出的是,當前線程結束後,對應該線程的局部變量將自動被垃圾回收,所以調用該方法清除線程的局部變量並不是必須的操作,但它可以加快內存回收的速度。

4. protected Object initialValue();//返回該線程局部變量的初始值,該方法是一個protected方法,顯然是爲了讓子類覆蓋而設計的。這個方法是一個延遲調用方法,在線程第1次調用get()或set(Object)時才執行,並且僅執行1次。如果不寫initialValue,那麼第一次調用get()會返回一個null。

5. public final static ThreadLocal resourse = new ThreadLocal();//resourse僅代表一個能夠存放String類型的ThreadLocal對象。此時不論什麼線程併發訪問這個變量,對它進行寫入,讀取操作,都是線程安全的。

由源碼一步一步畫出經典流程圖

我們根據ThreadLocal在實際開發中的使用流程,把網上到處傳遍的經典流程圖一步步畫出來。(認真看,百分百看懂吊打面試官!)

實際上,畫出這個圖,只需要三行代碼即可。注意:ThreadLocal設置爲局部方法僅僅爲了寫例子。ThreadLocal如果設置爲了局部變量將失去他本身將線程隔離的特性作用。完全就是核彈打螞蟻的操作!

首先,如第1步,我們new出一個ThreadLocal對象。

我們知道,ThreadLocal如果不進行set,是沒有任何數據的,於是我們進行步驟2開始set一個值。點進set看源碼!

點進ThreadLocal的set方法,我們發現它第一步就獲取了當前線程的對象。注意,這個當前線程的對象的生命週期是與當前線程同步的。於是更新流程圖:

然後我們根據當前線程對象,獲取了ThreadLocalMap(這個ThreadLocalMap並不是一直存在的,而是檢測我們當前現成是否存在這個ThreadLocalMap對象,如果不存在會先進行對象創建,否則直接獲取ThreadLocalMap對象)。於是更新流程:

在獲取map對象後,我們開始對當前線程的ThreadLocalMap對象進行set操作。

注意,此處的set的key是this。此時的this對象正是我們的ThreadLocal的對象,如圖所示:

那麼這個ThreadLocalMap對象的set方法又幹了些什麼呢?我們繼續進去看。

我們可以看到。我們把數據重新處理,放入了一個Entry數組中。那麼這個Entry數組又是什麼呢?先更新一下流程:

我們來看一下Entry類的結構。

我們可以看到,Entry的結構非常類似一個map,最最最重點的來了。就是這個Entry的key這裏是弱引用。what?弱引用?這是幹什麼用的?不要急,保持你的疑惑。我們先跟着上述步驟更新我們的流程圖:

終於,到了這一步,和我們最經典的圖相吻合了。這時候我們長出一口氣,總算完啦!不!我說沒完。還有最最關鍵的一步。

我們知道弱引用的特性是在一次GC後,與對象之間的聯繫斷開。那麼程序在運行一段時間,隨便發生一次GC後,整個內存圖是這樣的。這才最後內存中數據的分佈!

那有人又說?好傢伙,你圖都成這樣了,我再通過ref.get()方法獲取值還能獲取到嗎!稍安勿躁,這就帶你繼續看。

我們發現,誒當我們去get當前線程的ThreadLocal數據時,我們也是獲取當前線程,再次委託給我們的ThreadLocalMap去查詢。那麼流程是這樣的。

我們從步驟1的存在目的,進入當前線程的步驟2,去獲取當前線程key爲ref的value數據。有沒有茅塞頓開的感覺!這些總算可以收工了吧?當你準備長出一口氣時,我說還沒有!因爲博主一開始就有一個疑惑。就是我Entry的key執行ref對象的引用斷開時,我Entry中的key不會變爲null麼?答案我們繼續揭曉。

弱引用解讀

我們知道java中有強軟弱虛4種引用,而弱引用的定義就是隻要發生gc,那麼引用鏈就會斷開。我們來用程序測試一下弱引用。

首先,我們先隨意定義一個類測試類。

其次,我們使用弱引用引用這個類。我們測試以下程序在發生一次GC後,wrTest的結果是否爲null。

此時我們看到,該對象的確已經爲null了。此時,我們更換寫法。

誒?問題來了。爲什麼這個弱引用在發生一次GC後,值依然可以獲取到呢?是弱引用的引用鏈沒有消失麼?不,真相是我們此時的new Test()對象也恰巧被一個test強引用所指向,因此發生了GC也無法回收掉。這與我們ThreadLocal中,Entry的key斷開與new ThreadLocal()的引用鏈,卻依舊不爲null的場景完全吻合。

我們得到結論:即使弱引用所指向的對象與弱引用斷開引用鏈,但若是該對象有其他地方引用而導致無法回收,那麼我的弱引用依舊可以通過斷開前的連接地址去獲取值。(也就是說引用的斷開不會影響我們引用的尋址功能。引用的斷開只會導致引用鏈斷開導致對象被GC回收,但是!此時若有一個強引用引用着,那麼弱引用就可以在無引用鏈的情況下繼續訪問該對象。(這裏擴展一下。若對象的地址強制改變,弱引用將無法繼續跟蹤))。

舉一個簡單的案例:假設你買票上火車,找到了座位坐了進去。但是記性很差的你,上了個廁所回來找不到自己的座位了。此時,列車員始終可以根據你的購票檔案查到你的座位號。

到此爲止,ThreadLocal的源碼圖解可以告一段落了。

爲什麼ThreadLocalMap中的key要設置成弱引用?

ThreadLocal的被回收的場景

首先,強調一下這個假設的前提是ThreadLocal的用法使用不到位導致的,不優雅的。爲什麼博主這麼說呢?因爲ThreadLocal爲了可以擁有在每個線程直接獨立創建副本的能力,我們通常會把它用public static final進行修飾。也就是說這個引用不出意外將永遠不會消失。

有人會反駁說,雖然你這個引用用public static final進行修飾不會消失,但是線程會執行結束啊?如果仔細讀了上述流程的讀者應該已經很明確我們ThreadLocal獲取值是根據當前線程的ThreadLocalMap獲取的,如果當前線程結束,那麼該線程的ThreadLocalMap對象會一起消失。對應的Entry也會一起消失。(後續還有講解)

內存泄漏的原因

我們之前在講解流程的時候,講過ThreadMap中的Entry是弱引用。

那麼此時,我們逆向思考ThreadLocalMap中Entry的key是強引用,那麼當我們的ref出棧後,1號線斷開後,Entry就會始終有一個2號引用指向new ThreadLocal()對象,導致該對象永遠無法訪問,也無法回收,導致內存泄漏。

爲了避免這種尷尬,Entry的key與new ThreadLocal的對象設置爲弱引用。(咱哥倆聯繫一次就得了,以後找你討債沒問題,你是死是活我管不着)。着實把該對象當成了工具人!

設置爲弱引用後,經過一次GC內存模型如下:

此時,當ref出棧,new ThreadLoal孤立無援,唯有被回收的下場。到此,最常見的內存泄漏講解完畢。

很多網上的博客,都是這麼解析的。雖然光論結果來說都能說通,但是其實是本質對ThreadLocal並沒有深刻的理解。

當步驟1斷開後,步驟2再次經過垃圾回收斷開,對象才被孤立無援被回收。此處我很自信的說:2其實在1斷開之前就和對象徹底決裂分手再無瓜葛了!如果還沒理解,就繼續把我上述分析流程再看看。

Entry的key內存泄漏

我們之前看的博客說的最多的就是ThreadLocal對象的內存泄漏。然而其實我們發現Entry其實也有泄漏。如圖,由於我們將ThreadLocal對象的成功回收,這些我們的key”終於”變爲null了。但是我們的value依舊存在,因此這一組數據的value由於key爲null的原因也無法訪問導致內存泄漏。

呀!這可咋辦,之前看的博客沒人提過啊!別急,我們來看ThreadLocal是如何應對的。

Set優化

此時,當Entry的下標i對應的key值爲null的話,說明key已經被回收了,那麼直接把位置繼續佔用即可,反正key爲null已經沒用了。

Get優化

可以看到,get發現key爲null的處理方式是直接從Entry中強行刪除。

Remove方法

remove是我們主動觸發,清理Entry的方式。和get方法底層調用的是同一個方法。可以加速我們泄漏的內存回收。因此,如果當棧中的引用變爲null時,我們可以再次調用remove()方法,將ThreadLocalMap中的Entry進行清理。(更具時效性)

線程退出時優化

最後,當線程退出的時候,Thread類會進行清理操作。其中就包括清理ThreadLocalMap。

線程退出執行的exit()方法。

ThradLocal可以設置成局部變量,可以但沒意義,而且有內存泄漏風險

內存泄漏講了這麼這麼多!其實我們發現導致內存泄漏的原因就是這個ThreadLocal設置成了局部變量,導致ThreadLocal對象在線程結束前被回收。此時就會造成內存泄漏一直到線程結束纔可以釋放掉的風險。如果一定要這麼寫,那麼一定記得在ThreadLocal對象回收時調用一下remove()方法及時釋放內存。

另外,threadLocal如果設置成局部變量,那麼同一個線程中的其他方法也無法獲取當該對象。這樣也就背離了ThreadLocal在同一個線程下,共享同一個變量的設計初衷了。核彈殺螞蟻。

ThreadLocal的錯誤使用導致線程不安全

由圖可見,當ThreadLocal操作相同對象的時候,所有的操作都指向同一個實例。如果想讓上面的程序正常運行,需要每一個ThreadLocal都持有一個新的實例。

總結

其實平時我們從書本中獲取到ThreadLocal知識足以面對我們應付各種場景的面試了。但是筆者最開始即使大致清楚了ThreadLocal的大致工作流程,卻有許多細節沒有串起來。本文的目的不僅僅是讓各位讀者擁有應付面試的能力,更是帶着大家比較精細的分析了ThreadLocal的設計思路。我們往往學習一門新的技術時,要站在這個技術出現之前的開發人員面臨的問題。ThreadLocal就解決了同一線程中的數據共享問題。

那麼我們要解決同一線程間數據的共享問題,我們就需要拿到這個線程所有的方法共享的對象。於是我們開發人員在操作ThreadLocal的絕大部分方法時,第一步永遠是獲取當前線程對象。再由這個當前線程對象維護一個類似於Map的Entry。以ThreadLocal對象作爲key,存放僅僅屬於當前線程的value,從而達到線程分離。

我們要完全弄懂ThreadLocal,不能跟隨很多博客上講的,上來直接就硬着頭皮開始解決弱引用的問題。我們首先要先把自己幻想成開發人員,一步一步在腦袋中畫出ThreadLocal的工作流程。把ThreadLocalMap的Entry的key引用ThreadLocal對象的圖像模擬出來(流程如果有還是不太清楚的朋友,可以再仔細看看上文講解的流程圖)。

此時,我們很明確的知道了ThreadLocalMap的Entry的key引用ThreadLocal對象這條引用存在的意義了,但是,如果這條引用設置成強引用就不可避免的導致我們的ThreadLocal對象發生了內存泄漏。於是我們纔想到了使用弱引用去解決內存泄漏問題。

同時,通過講解弱的例子,我們瞭解到只要被弱引用引用過的對象,即使經過GC導致弱引用鏈斷開,只要該對象仍有強引用引用着讓它不被GC,那麼弱引用依舊不會爲null的小細節。

希望通過這個TL這個重點知識,幫助歸納吸收更多解決問題的思路。吊打面試官和那條”該死”的弱引用一樣,只是順手搞定的事兒了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章