ThreadLocal和Synchonized都用於解決多線程併發訪問。但是ThreadLocal與Synchronized有着本質的區別。Synchronized是利用鎖的機制,使變量或代碼代碼塊在某一個時刻僅僅能被一個線程訪問。java
從名字咱們就能夠看到ThreadLocal叫作線程變量,意思是ThreadLocal中填充的變量屬於當前線程,該變量對其餘線程而言是隔離的。ThreadLocal爲變量在每一個線程中都建立了一個副本,那麼每一個線程能夠訪問本身內部的副本變量。面試
從字面意思很是容易理解,可是從實際使用的角度來看就沒那麼容易了。做爲一個面試常問的點,使用場景那也是至關的豐富。數據庫
如今應該對ThreadLocal已經有一個大概的認識了。下面看看具體如何使用。數組
既然ThreadLocal的做用是每個線程建立一個副本,那咱們使用一個例子來驗證一下:安全
從結果可知,每個線程都有各自的local值。也就是說,threadLocal的值是線程與線程分離的。具體原理能夠畫出如下不一樣線程中ThreadLocalMap是如何存儲數據的。服務器
若是是第一次學習ThreadLocal的朋友可能看懵了,ThreadLocal我都沒看懂,你跟我說ThreadLocalMap?別急,咱們接着往下看。markdown
這裏整理了最近BAT最新面試題,2021船新版本!!須要的朋友能夠點擊:這個,點這個!!,備註:jj。但願那些有須要朋友能在今年第一波招聘潮找到一個本身滿意順心的工做!
多線程
咱們知道,數據庫鏈接池最爲咱們詬病的就是鏈接的建立與關閉。這其中要耗費大量的資源與時間。咱們的ThreadLocal也能夠幫咱們解決這個問題。併發
這是一個數據庫鏈接的管理類。咱們在使用數據庫的時候首先就是創建數據庫鏈接。而後用完了以後就關閉。這樣作有一個很嚴重的問題,若是有1個用戶頻繁使用數據庫,那麼就須要創建屢次鏈接和關閉。這樣咱們服務器可能吃不消,那麼怎麼辦呢?若是一萬個客戶端,那麼服務器壓力更大。工具
這時最好使用ThreadLocal。由於ThreadLocal在每一個線程中會建立一個副本。而且在線程內部任何地方可使用。線程之間互不影響。這樣一來就不存在線程安全問題,也不會嚴重影響程序執行性能,避免了connection的頻繁建立和銷燬。(固然實際中咱們有數據庫鏈接池能夠處理,但咱們的目的都很明確,避免鏈接對象的頻繁建立與銷燬!)
以上主要講解了一個基本的案例,而後還分析了爲何在數據庫鏈接的時候會使用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的源碼圖解能夠告一段落了。
首先,強調一下這個假設的前提是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斷開以前就和對象完全決裂分手再無瓜葛了!若是還沒理解,就繼續把我上述分析流程再看看。
咱們以前看的博客說的最多的就是ThreadLocal對象的內存泄漏。然而其實咱們發現Entry其實也有泄漏。如圖,因爲咱們將ThreadLocal對象的成功回收,這些咱們的key」終於」變爲null了。可是咱們的value依舊存在,所以這一組數據的value因爲key爲null的緣由也沒法訪問致使內存泄漏。
呀!這可咋辦,以前看的博客沒人提過啊!別急,咱們來看ThreadLocal是如何應對的。
此時,當Entry的下標i對應的key值爲null的話,說明key已經被回收了,那麼直接把位置繼續佔用便可,反正key爲null已經沒用了。
能夠看到,get發現key爲null的處理方式是直接從Entry中強行刪除。
remove是咱們主動觸發,清理Entry的方式。和get方法底層調用的是同一個方法。能夠加速咱們泄漏的內存回收。所以,若是當棧中的引用變爲null時,咱們能夠再次調用remove()方法,將ThreadLocalMap中的Entry進行清理。(更具時效性)
最後,當線程退出的時候,Thread類會進行清理操做。其中就包括清理ThreadLocalMap。
線程退出執行的exit()方法。
內存泄漏講了這麼這麼多!其實咱們發現致使內存泄漏的緣由就是這個ThreadLocal設置成了局部變量,致使ThreadLocal對象在線程結束前被回收。此時就會形成內存泄漏一直到線程結束才能夠釋放掉的風險。若是必定要這麼寫,那麼必定記得在ThreadLocal對象回收時調用一下remove()方法及時釋放內存。
另外,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這個重點知識,幫助概括吸取更多解決問題的思路。吊打面試官和那條」該死」的弱引用同樣,只是順手搞定的事兒了。