阿里架構師教你如何使用ThreadLocal及原理分析

阿里架構師教你如何使用ThreadLocal及原理分析

 

內容導航算法

  • 什麼是ThreadLocal
  • ThreadLocal的使用
  • 分析ThreadLocal的實現原理
  • ThreadLocal的應用場景及問題

1、什麼是ThreadLocal

ThreadLocal,簡單翻譯過來就是本地線程,可是直接這麼翻譯很難理解ThreadLocal的做用,若是換一種說法,能夠稱爲線程本地存儲。簡單來講,就是ThreadLocal爲共享變量在每一個線程中都建立一個副本,每一個線程能夠訪問本身內部的副本變量。這樣作的好處是能夠保證共享變量在多線程環境下訪問的線程安全性數據庫

2、ThreadLocal的使用演示

ThreadLocal的使用數組

沒有使用ThreadLocal時安全

經過一個簡單的例子來演示一下ThreadLocal的做用,這段代碼是定義了一個靜態的成員變量 num,而後經過構造5個線程對這個 num作遞增性能優化

阿里架構師教你如何使用ThreadLocal及原理分析

 

運行結果session

阿里架構師教你如何使用ThreadLocal及原理分析

 

每一個線程都會對這個成員變量作遞增,若是線程的執行順序不肯定,那麼意味着每一個線程得到的結果也是不同的。多線程

使用了ThreadLocal之後架構

經過ThreadLocal對上面的代碼作一個改動併發

阿里架構師教你如何使用ThreadLocal及原理分析

 

運行結果分佈式

阿里架構師教你如何使用ThreadLocal及原理分析

 

從結果能夠看到,每一個線程的值都是5,意味着各個線程都是從ThreadLocal的 initialValue方法中拿到默認值0而且作了 num+=5的操做,同時也意味着每一個線程從ThreadLocal中拿到的值都是0,這樣使得各個線程對於共享變量num來講,是徹底隔離彼此不相互影響.

ThreadLocal會給定一個初始值,也就是 initialValue()方法,而每一個線程都會從ThreadLocal中得到這個初始化的值的副本,這樣可使得每一個線程都擁有一個副本拷貝

3、從源碼分析ThreadLocal的實現

看到這裏,估計有不少人都會和我同樣有一些疑問

  1. 每一個線程的變量副本是怎麼存儲的?
  2. ThreadLocal是如何實現多線程場景下的共享變量副本隔離?

帶着疑問,來看一下ThreadLocal這個類的定義(默認狀況下,JDK的源碼都是基於1.8版本)

阿里架構師教你如何使用ThreadLocal及原理分析

 

從ThreadLocal的方法定義來看,仍是挺簡單的。就幾個方法

  • get: 獲取ThreadLocal中當前線程對應的線程局部變量
  • set:設置當前線程的線程局部變量的值
  • remove:將當前線程局部變量的值刪除

另外,還有一個initialValue()方法,在前面的代碼中有演示,做用是返回當前線程局部變量的初始值,這個方法是一個 protected方法,主要是在構造ThreadLocal時用於設置默認的初始值

set方法的實現

set方法是設置一個線程的局部變量的值,至關於當前線程經過set設置的局部變量的值,只對當前線程可見。

阿里架構師教你如何使用ThreadLocal及原理分析

 

  • Thread.currentThread 獲取當前執行的線程
  • getMap(t) ,根據當前線程獲得當前線程的ThreadLocalMap對象,這個對象具體是作什麼的?稍後分析
  • 若是map不爲空,說明當前線程已經構造過ThreadLocalMap,直接將值存儲到map中
  • 若是map爲空,說明是第一次使用,調用 createMap構造

ThreadLocalMap是什麼?

咱們來分析一下這句話, ThreadLocalMapmap=getMap(t)得到一個ThreadLocalMap對象,那這個對象是幹嗎的呢?

其實不用分析,基本上也能猜想出來,Map是一個集合,集合用來存儲數據,那麼在ThreadLocal中,應該就是用來存儲線程的局部變量的。 ThreadLocalMap這個類很關鍵。

阿里架構師教你如何使用ThreadLocal及原理分析

 

t.threadLocals實際上就是訪問Thread類中的ThreadLocalMap這個成員變量

阿里架構師教你如何使用ThreadLocal及原理分析

 

從上面的代碼發現每個線程都有本身單獨的ThreadLocalMap實例,而對應這個線程的全部本地變量都會保存到這個map內

ThreadLocalMap是在哪裏構造?

在 set方法中,有一行代碼 createmap(t,value);,這個方法就是用來構造ThreadLocalMap,從傳入的參數來看,它的實現邏輯基本也能猜出出幾分吧

阿里架構師教你如何使用ThreadLocal及原理分析

 

Threadt 是經過 Thread.currentThread()來獲取的表示當前線程,而後直接經過 newThreadLocalMap將當前線程中的 threadLocals作了初始化

ThreadLocalMap是一個靜態內部類,內部定義了一個Entry對象用來真正存儲數據

阿里架構師教你如何使用ThreadLocal及原理分析

 

分析到這裏,基本知道了ThreadLocalMap長啥樣了,也知道它是如何構造的?那麼我看到這裏的時候仍然有疑問

  • Entry集成了 WeakReference,這個表示什麼意思?
  • 在構造ThreadLocalMap的時候 newThreadLocalMap(this,firstValue);,key實際上是this,this表示當前對象的引用,在當前的案例中,this指的是 ThreadLocal<Integer>local。那麼多個線程對應同一個ThreadLocal實例,怎麼對每個ThreadLocal對象作區分呢?

解惑WeakReference

weakReference表示弱引用,在Java中有四種引用類型,強引用、弱引用、軟引用、虛引用。

使用弱引用的對象,不會阻止它所指向的對象被垃圾回收器回收。

在Java語言中, 當一個對象o被建立時, 它被放在Heap裏. 當GC運行的時候, 若是發現沒有任何引用指向o, o就會被回收以騰出內存空間. 也就是說, 一個對象被回收, 必須知足兩個條件:

  • 沒有任何引用指向它
  • GC被運行.

這段代碼中,構造了兩個對象a,b,a是對象DemoA的引用,b是對象DemoB的引用,對象DemoB同時還依賴對象DemoA,那麼這個時候咱們認爲從對象DemoB是能夠到達對象DemoA的。這種稱爲強可達(strongly reachable)

阿里架構師教你如何使用ThreadLocal及原理分析

 

若是咱們增長一行代碼來將a對象的引用設置爲null,當一個對象再也不被其餘對象引用的時候,是會被GC回收的,可是對於這個場景來講,即時是a=null,也不可能被回收,由於DemoB依賴DemoA,這個時候是可能形成內存泄漏的

阿里架構師教你如何使用ThreadLocal及原理分析

 

經過弱引用,有兩個方法能夠避免這樣的問題

阿里架構師教你如何使用ThreadLocal及原理分析

 

對於方法2來講,DemoA只是被弱引用依賴,假設垃圾收集器在某個時間點決定一個對象是弱可達的(weakly reachable)(也就是說當前指向它的全都是弱引用),這時垃圾收集器會清除全部指向該對象的弱引用,而後把這個弱可達對象標記爲可終結(finalizable)的,這樣它隨後就會被回收。

試想一下若是這裏沒有使用弱引用,意味着ThreadLocal的生命週期和線程是強綁定,只要線程沒有銷燬,那麼ThreadLocal一直沒法回收。而使用弱引用之後,當ThreadLocal被回收時,因爲Entry的key是弱引用,不會影響ThreadLocal的回收防止內存泄漏,同時,在後續的源碼分析中會看到,ThreadLocalMap自己的垃圾清理會用到這一個好處,方便對無效的Entry進行回收

解惑ThreadLocalMap以this做爲key

在構造ThreadLocalMap時,使用this做爲key來存儲,那麼對於同一個ThreadLocal對象,若是同一個Thread中存儲了多個值,是如何來區分存儲的呢?

答案就在 firstKey.threadLocalHashCode&(INITIAL_CAPACITY-1)

阿里架構師教你如何使用ThreadLocal及原理分析

 

關鍵點是 threadLocalHashCode,它至關於一個ThreadLocal的ID,實現的邏輯以下

阿里架構師教你如何使用ThreadLocal及原理分析

 

這裏用到了一個很是完美的散列算法,能夠簡單理解爲,對於同一個ThreadLocal下的多個線程來講,當任意線程調用set方法存入一個數據到Entry中的時候,其實會根據 threadLocalHashCode生成一個惟一的id標識對應這個數據,存儲在Entry數據下標中。

  • threadLocalHashCode是經過
  • nextHashCode.getAndAdd(HASH_INCREMENT)來實現的
  • i*HASH_INCREMENT+HASH_INCREMENT,每次新增一個元素(ThreadLocal)到Entry[],都會自增0x61c88647,目的爲了讓哈希碼能均勻的分佈在2的N次方的數組裏
  • Entry[i]= hashCode & (length-1)

魔數0x61c88647

從上面的分析能夠看出,它是在上一個被構造出的ThreadLocal的threadLocalHashCode的基礎上加上一個魔數0x61c88647。咱們來作一個實驗,看看這個散列算法的運算結果

阿里架構師教你如何使用ThreadLocal及原理分析

 

輸出結果

阿里架構師教你如何使用ThreadLocal及原理分析

 

根據運行結果,這個算法在長度爲2的N次方的數組上,確實能夠完美散列,沒有任何衝突, 是否是很神奇。

魔數0x61c88647的選取和斐波那契散列有關,0x61c88647對應的十進制爲1640531527。而斐波那契散列的乘數能夠用 (long)((1L<<31)*(Math.sqrt(5)-1)); 若是把這個值給轉爲帶符號的int,則會獲得-1640531527。也就是說(long)((1L<<31)*(Math.sqrt(5)-1));獲得的結果就是1640531527,也就是魔數0x61c88647

阿里架構師教你如何使用ThreadLocal及原理分析

 

總結,咱們用0x61c88647做爲魔數累加爲每一個ThreadLocal分配各自的ID也就是threadLocalHashCode再與2的冪取模,獲得的結果分佈很均勻。

圖形分析

爲了更直觀的體現 set方法的實現,經過一個圖形表示以下

阿里架構師教你如何使用ThreadLocal及原理分析

 

set剩餘源碼分析

前面分析了set方法第一次初始化ThreadLocalMap的過程,也對ThreadLocalMap的結構有了一個全面的瞭解。那麼接下來看一下map不爲空時的執行邏輯

阿里架構師教你如何使用ThreadLocal及原理分析

 

主要邏輯

  • 根據key的散列哈希計算Entry的數組下標
  • 經過線性探索探測從i開始日後一直遍歷到數組的最後一個Entry
  • 若是map中的key和傳入的key相等,表示該數據已經存在,直接覆蓋
  • 若是map中的key爲空,則用新的key、value覆蓋,並清理key=null的數據
  • rehash擴容

replaceStaleEntry

因爲Entry的key爲弱引用,若是key爲空,說明ThreadLocal這個對象被GC回收了。 replaceStaleEntry的做用就是把陳舊的Entry進行替換

阿里架構師教你如何使用ThreadLocal及原理分析

 

cleanSomeSlots

這個函數有兩處地方會被調用,用於清理無效的Entry

  • 插入的時候可能會被調用
  • 替換無效slot的時候可能會被調用

區別是前者傳入的n爲元素個數,後者爲table的容量

阿里架構師教你如何使用ThreadLocal及原理分析

 

expungeStaleEntry

執行一次全量清理

阿里架構師教你如何使用ThreadLocal及原理分析

 

get操做

set的邏輯分析完成之後,get的源碼分析就很簡單了

阿里架構師教你如何使用ThreadLocal及原理分析

 

setInitialValue

根據 initialValue()的value初始化ThreadLocalMap

阿里架構師教你如何使用ThreadLocal及原理分析

 

  • 從當前線程中獲取ThreadLocalMap,查詢當前ThreadLocal變量實例對應的Entry,若是不爲null,獲取value,返回
  • 若是map爲null,即尚未初始化,走初始化方法

remove方法

remove的方法比較簡單,從Entry[]中刪除指定的key就行

阿里架構師教你如何使用ThreadLocal及原理分析

 

4、ThreadLocal的應用場景及問題

應用場景

ThreadLocal的實際應用場景:

  1. 好比在線程級別,維護session,維護用戶登陸信息userID(登錄時插入,多個地方獲取)
  2. 數據庫的連接對象 Connection,能夠經過ThreadLocal來作隔離避免線程安全問題

問題

ThreadLocal的內存泄漏

ThreadLocalMap中Entry的key使用的是ThreadLocal的弱引用,若是一個ThreadLocal沒有外部強引用,當系統執行GC時,這個ThreadLocal勢必會被回收,這樣一來,ThreadLocalMap中就會出現一個key爲null的Entry,而這個key=null的Entry是沒法訪問的,當這個線程一直沒有結束的話,那麼就會存在一條強引用鏈

阿里架構師教你如何使用ThreadLocal及原理分析

 

Thread Ref - > Thread -> ThreadLocalMap - > Entry -> value 永遠沒法回收而形成內存泄漏

其實咱們從源碼分析能夠看到,ThreadLocalMap是作了防禦措施的

  • 首先從ThreadLocal的直接索引位置(經過
  • ThreadLocal.threadLocalHashCode & (len-1)運算獲得)獲取Entry e,若是e不爲null而且key相同則返回e
  • 若是e爲null或者key不一致則向下一個位置查詢,若是下一個位置的key和當前須要查詢的key相等,則返回對應的Entry,不然,若是key值爲null,則擦除該位置的Entry,不然繼續向下一個位置查詢

在這個過程當中遇到的key爲null的Entry都會被擦除,那麼Entry內的value也就沒有強引用鏈,天然會被回收。仔細研究代碼能夠發現,set操做也有相似的思想,將key爲null的這些Entry都刪除,防止內存泄露。

可是這個設計一來與一個前提條件,就是調用get或者set方法,可是不是全部場景都會知足這個場景的,因此爲了不這類的問題,咱們能夠在合適的位置手動調用ThreadLocal的remove函數刪除不須要的ThreadLocal,防止出現內存泄漏

因此建議的使用方法是

  • 將ThreadLocal變量定義成private static的,這樣的話ThreadLocal的生命週期就更長,因爲一直存在ThreadLocal的強引用,因此ThreadLocal也就不會被回收,也就能保證任什麼時候候都能根據ThreadLocal的弱引用訪問到Entry的value值,而後remove它,防止內存泄露
  • 每次使用完ThreadLocal,都調用它的remove()方法,清除數據。

推薦一個交流學習交流圈子:142019080 裏面會分享一些資深架構師錄製的視頻錄像:有Spring,MyBatis,Netty源碼 分析,高併發、高性能、分佈式、微服務架構的原理,JVM性能優化這些成爲架構師必備的知識體系。還能領取免費的 學習資源,目前受益良多

相關文章
相關標籤/搜索