Dubbo中的InternalThreadLocal的簡單分析

Dubbo中存在一些優化設計,這些設計具備必定的參考價值,這裏調研下 InternalThreadLocal 的優化設計。apache

 

如下內容的章節爲:數組

  1. ThreadLocal的介紹
  2. InternalThreadLocal的介紹
  3. InternalThreadLocal和ThreadLocal的對比和使用範圍
  4. 垃圾回收的考慮

 

1.ThreadLocal的介紹

查看org.apache.dubbo.common.threadlocal.InternalThreadLocal 的時候,會發現這裏有一個特殊的設計,說是參考Netty的優化設計,對ThreadLocal進行了優化。安全

ThreadLocal咱們知道能夠經過這個類在一個線程內共享線程級變量,這裏簡單地介紹下線程級變量的實現原理。以下圖所示:多線程

 

 

假設有一個ThreadLocal<User> currentLocalUser 的線程級變量,那麼這個線程級變量的存儲邏輯爲:函數

  • 獲取當前的執行Thread,獲取其內部變量threadLocals,這是一個Map結構,可是這個Map是一個自定義的Map,與咱們常用的TreeMap和HashMap不是一回事。
  • threadLocals的Map結構,內部由Entry組成,能夠簡單理解爲一個Pair<Key,Value>的鍵值對。Key能夠理解爲ThreadLocal對象(其實還有弱引用。後面再講),Value能夠理解爲ThreadLocal的泛型裏表示的線程級變量。
  • 在鍵值對的基礎上,如何實現Map查詢?threadLocals是一個Hash表的結構,內部是一個固定長度Entry數組,Key經過計算hash,映射到一個Entry的index上,實現定位。
  • 提到hash表的實現,就必須考慮到如何解決碰撞問題??JDK的HashMap採用的是拉鍊法和紅黑樹,這裏的作法是開放地址法,直接在計算出index上+1,再次嘗試插入,看是否碰撞,直至成功。
  • 若是採用開放地址法,那麼必須考慮擴容問題,不然若是map塞滿了,一直用開放地址法,也找不到空閒的位置了,這裏採用的方式是有一個閾值,默認2/3的負載因子,超過就*2進行擴容。
  • threadLocals採用的是類hashmap的實現方式,那麼其hash性能就很重要,這裏有一個有趣的數字0x61c88647,它的值是2的32次方乘以0.618黃金比例。每次有set一個新key的時候,內部有個全局計算器,加上0x61c88647以後的值,除之內部的Entry數組的長度,做爲hash過的index的值。Entry數組的長度是2的次方,每次擴容也是乘以2。你們能夠查一下資料,0x61c88647是一個性能比較好的數值,在不停累加,並且取2的整數次方的餘數後,離散性能很好,這裏再也不贅述,網上不少文章,我也不能徹底推理出來。

總結一下,若是有一個ThreadLocal的線程級變量要初始化並插入,那麼其簡單步驟爲:性能

  1. 經過當前的線程,獲取其內部的threadLocals的Map結構。
  2. 這個ThreadLocal的內部變量threadLocalHashCode初始化,每次在全局計數器的基礎上增長0x61c88647這個魔數,做爲threadLocalHashCode的值。
  3. 計算出的threadLocalHashCode對當前的Map裏的Entry數組的長度取餘數,就獲得具體的index下標的存儲位置。固然了,若是擴容了,會有resize來處理,這裏只討論簡單狀況,不考慮擴容和hash碰撞的問題。
  4. 建立出一個Entry對象,能夠簡單理解爲Pair<WeakReference<ThreadLocal>,User> 這麼一個鍵值對,Entry就放在上一步計算的index的Entry數組裏。
  5. 線程級變量插入完畢,繼續其餘的業務。
  6. 這裏會發現,對這個Map的操做沒有加鎖操做,是由於這個Map原本就是當前的線程的內部變量,只有當前線程能夠操做內部的Map,每一個線程都操做本身的map,故沒有線程間共享搶佔的問題。

 

若是想要獲取currentLocalUser當前的線程級變量裏的user,其簡單步驟爲:優化

  1. 經過當前的線程,獲取其內部的threadLocals的Map結構。
  2. 經過currentLocalUser這個ThreadLocal,獲得其內部變量threadLocalHashCode,除以Map結構的Entry數組的長度,獲取其index,獲取一個Entry對象,裏面是Pair<WeakReference<ThreadLocal>,User>這種結構。
  3. 注意:這是hash版本的Map,那麼可能存在碰撞,因此會判斷WeakReference<ThreadLocal>裏的ThreadLocal對象,是否是currentLocalUser這個變量,若是不是,+1計算下一個index,直至相等,此時取出User對象
  4. 此時的這個User對象就是線程級變量。若是這個User對象沒有經過其餘的引用與其餘的線程分享,那麼User就是線程安全的。

總體總結:線程

經過自增一個特殊數字的方式,再對數組取餘,實現了hash元素定位,在衝突的時候,採用+1的方式,再次定位,直至找到一個空閒的槽位,或者在查找的時候,直至吵到等於本對象的槽位。你可能意識到,這種hash的方式確定會存在碰撞,並且碰撞後,經過自增的方式重定位以後,極端狀況下存在線性時間複雜度的開銷,一直有衝突,須要找到本身的槽位。設計

 

 

2.InternalThreadLocal的介紹

Dubbo存在一種InternalThreadLocal的優化對象,來模擬ThreadLocal的操做,來優化性能,他的基本結構和JDK本身的方案差很少,只是在hash方案上有些出入。對象

上面提到,ThreadLocal與Thread線程裏的內部變量threadLocals有關係,那麼Thread線程與InternalThreadLocal壓根不要緊,因此須要InternalThread的配合,InternalThread繼承了Thread對象,內部增長了一個threadLocalMap的內部Map對象,來存在InternalThreadLocal和對象的線程級變量的映射關係。InternalThreadLocal和InternalThread的關係,與ThreadLocal和Thread的關係幾乎同樣,這裏再也不畫圖。

重點在InternalThread內部的map的hash方案上,這裏的hash方案也是index的自增,不過不是自增0x61c88647,而是直接加1,每次計算出的index,就是內部的Map的Entry數組的下標。直接定位,不會出現衝突。由於:每一個InternalThreadLocal對象的index的值,都是自增的,是不會出現衝突的。

 

那麼問題又來了:若是每次都自增,隨着程序的運行,這個index會不會愈來愈大,內部的Entry數組愈來愈大,最後OOM?

答案是:正常狀況下不會。經過檢索Dubbo的源碼,會發現全部的InternalThreadLocal的使用,都是static的使用,因此InternalThreadLocal實例的個數是肯定的,其index也不會無限制的增長。

總體總結:

InternalThreadLocal做爲dubbo內部的高性能的線程級變量的實現,雖然表面上是Map的名字,其實是數組的訪問速度。因此在多線程頻繁訪問線程級變量的狀況下,必定速度很快。可是也有侷限性,最好做爲dubbo的內部變量使用,外部不要直接使用。若是外部確實要使用,也要使用static的方式,若是伴隨着業務代碼,一直在new InternalThreadLocal,會形成內部的index一直累加,致使Map內部的數組也一直膨脹,直到OOM。就算不OOM,內部也會觸發邏輯:

 

 

3.InternalThreadLocal和ThreadLocal的對比和使用範圍

 

  優點 劣勢
InternalTreadLocal 速度高,數組訪問級別的速度。由於沒有hash碰撞的問題,性能一直是O(1)

在dubbo內部使用,最好不要在本身的代碼中使用

確實要使用,使用static的方式,防止OOM

ThreadLocal 通用性強 在沒有碰撞的狀況下,訪問速度是O(1),在最壞的狀況下,訪問速度接近於O(map的容量)

 

 

 4.垃圾回收的考慮

通過簡單分析這個結構後,這個線程級共享變量機制的一個重要問題是垃圾回收。Java不是自動垃圾回收麼,爲何要考慮垃圾回收?

由於這些線程級變量是跟線程有關的,而在GC的時候,JVM掃描變量可達性的時候,部分可達性分析會以Thread爲根開始掃描,此部分能夠搜索GC ROOT的概念。與GC ROOT有強引用的內存是不會回收的。

先分析ThreadLocal的垃圾回收場景:

  • 一個正在執行的線程確定是不能夠回收的,那麼Thread內部的threadLocals的這個Map結構確定也不會回收的,這是一個強引用StrongReference;
  • map結構的內部,主要分爲3部分:Entry結構體(至關於Pair<Key,Value>結構),Key部分就是WeakReference<ThreadLocal>,value部分就是ThreadLocal的泛型內表達的值。
  • Entry部分回收不計,這部分的回收取決於內部的keyValue的回收,在回收的時候,一併回收。
  • Key部分很特殊,是WeakReference弱引用,弱引用的部分,若是GC的時候,內部的ThreadLocal會被回收。可是,業務邏輯執行中,是不會被回收的,分爲兩種狀況:若是你聲明的ThreadLocal是static的,那麼存在一個永生帶到這個static的ThreadLocal實例的強引用,而永生帶屬於GC ROOT的一部分,跟Thread做爲GC ROOT的待遇同樣。若是ThreadLocal是new的臨時變量,在業務邏輯執行中,JVM的棧上會對這個臨時變量有一個強應用,棧區也是GC ROOT的一部分,因此在業務過程當中,WeakReference即便想回收ThreadLocal也不會真的回收。
  • 可是若是業務邏輯執行完了,並且ThreadLocal是new出來的臨時變量,那麼其ThreadLocal的實例可能被回收,此時,WeakReference<ThreadLocal>的Key部分,若是查詢,會發現存儲的ThreadLocal已經爲null。此時問題來了:Entry做爲Map內部的數組的一部分,是一個強引用,而Value部分是Entry的一個強引用,此時Entry和Value都沒法回收,豈不是形成了內存泄漏的問題,可是實際上正常使用是不會內存泄漏的,由於ThreadLocal的set和get方法內部自帶了垃圾回收,若是發現key部分已經回收了,就把value置爲null,entry置爲null,幫助JVM回收。
  • ThreadLocal的回收部分實際更復雜,能夠搜索【ThreadLocal set get】檢索文章查看細節,更復雜的地方是:插入的時候,有hash碰撞,採用了index+1的開放地址法處理衝突,若是中間有個Entry回收了,須要把後面的有效的Entry向前移動,不然後面的節點在在get的時候,用index+1的方式進行探測的時候,會增長額外的代碼複雜度和存儲空間消耗。

 

再分析InternalThreadLocal的垃圾回收場景:

答案是沒有這麼複雜的垃圾回收,由於沒有垃圾產生。

考慮上面的結論:InternalTreadLocal內部使用一個數組,並且set和get均使用一個index的下標方式(不是hash的方式,再對數組長度取餘),直接進行讀寫,並且都是static的方式,InternalTreadLocal實例是不會被JVM回收的,能夠理解爲Key部分不會被回收,只有Value部分可能被臨時覆蓋,致使老值被回收。

這裏也從另一個側面解釋了爲何InternalTreadLocal更快:由於InternalTreadLocal的set和get就是數組直接訪問,且根本不考慮垃圾回收。ThreadLocal要清理裏面的Map的垃圾數據,又沒有定時線程主動觸發清理(實際上也沒有其餘線程可用,由於每一個線程只能管本身的map結構),只能依賴set和get函數來被動地觸發垃圾清理,更致使了性能在極限狀況下更慢一點。