Java -- 基於JDK1.8的ThreadLocal源碼分析

1,最近在作一個需求的時候須要對外部暴露一個值得應用  ,通常來講直接寫個單例,將這個成員變量的值暴露出去就ok了,可是當時忽然靈機一動(如今回想是個多餘的想法),想到handle源碼裏面有使用過ThreadLocal這個類,想了想爲何不想直接用ThreadLocal保存數據源而後使用靜態方法暴露出去呢,結果發現使用ThreadLocal有時候會獲取不到值,查了下緣由原來同事是在子線程中調用的(捂臉哭泣),因此仍是要來看一波源碼,看看ThreadLocal底層實現,適用於哪些場景java

 

2,咱們如今網上搜索一下前人對於ThreadLocal這個類的一些總結數據庫

ThreadLocal特性及使用場景:
1、方便同一個線程使用某一對象,避免沒必要要的參數傳遞;
2、線程間數據隔離(每一個線程在本身線程裏使用本身的局部變量,各線程間的ThreadLocal對象互不影響);
三、獲取數據庫鏈接、Session、關聯ID(好比日誌的uniqueID,方便串起多個日誌);

從上面的總結來看,主要是用來線程間的數據隔離的,即ThreadLocal 對象能夠在多個線程中共享, 但每一個線程只能讀寫其中本身的數據副本。數組

ThreadLocal<Boolean> mBooleanThreadLocal = new ThreadLocal<>();
        mBooleanThreadLocal.set(true);
        Boolean result = mBooleanThreadLocal.get();

 主要就是這三個方法  ,那我們就一個一個來看併發

2.1 構造函數less

 /**
     * ThreadLocals rely on per-thread linear-probe hash maps attached
     * to each thread (Thread.threadLocals and
     * inheritableThreadLocals).  The ThreadLocal objects act as keys,
     * searched via threadLocalHashCode.  This is a custom hash code
     * (useful only within ThreadLocalMaps) that eliminates collisions
     * in the common case where consecutively constructed ThreadLocals
     * are used by the same threads, while remaining well-behaved in
     * less common cases.
     */
    private final int threadLocalHashCode = nextHashCode();

    /**
     * The next hash code to be given out. Updated atomically. Starts at
     * zero.
     */
    private static AtomicInteger nextHashCode =
        new AtomicInteger();

    /**
     * The difference between successively generated hash codes - turns
     * implicit sequential thread-local IDs into near-optimally spread
     * multiplicative hash values for power-of-two-sized tables.
     */
    private static final int HASH_INCREMENT = 0x61c88647;

    public ThreadLocal() {
    }

  ThreadLocal的構造方法是一個空方法 ,可是有三個參數,nextHashCode 和HASH_INCREMENT 是ThreadLocal類的靜態變量,真正變量只有 threadLocalHashCode 這一個,這三個參數都不是善茬啊。函數

  HASH_INCREMENT 英文註釋解釋是「連續生成的哈希碼之間的差別——將隱式順序線程本地id轉換爲接近最優擴散的乘法哈希值,用於大小爲2的冪的表。」 這句解釋看得咱們一臉矇蔽啊,不過我記得看HashMap源碼的時候 有解決哈希衝突這一說,咱們先不探究這麼多,先將0x61c88647這個奇怪的值記在內心一下。this

  nextHashCode 的表示了即將分配的下一個ThreadLocal實例的threadLocalHashCode 的值。atom

  threadLocalHashCode 見名知意 這個ThreadLocal對象的hashcodespa

private static int nextHashCode() {
    return nextHashCode.getAndAdd(HASH_INCREMENT);
}

  調用的是nextHashCode方法  ,就是將ThreadLocal類的下一個hashCode值即nextHashCode的值賦給實例的threadLocalHashCode,而後nextHashCode的值增長HASH_INCREMENT這個值。線程

  咱們先無論這些參數的生產方式,先知道有這三個參數就行,繼續往下面看流程

2,2 set方法

 1 public void set(T value) {
 2         Thread t = Thread.currentThread();
 3         ThreadLocalMap map = getMap(t);
 4         if (map != null)
 5             map.set(this, value);
 6         else
 7             createMap(t, value);
 8     }
 9 
10 ThreadLocalMap getMap(Thread t) {
11         return t.threadLocals;
12     }
13 
14 void createMap(Thread t, T firstValue) {
15         t.threadLocals = new ThreadLocalMap(this, firstValue);
16     }

   咱們能夠看到,首先經過當前調用的線程獲取到線程中對應的threadLocals變量(這裏我一臉懵逼  線程中居然使用到ThreadLocal了,趕忙去看看線程的源碼)

public class Thread implements Runnable {
     ThreadLocal.ThreadLocalMap threadLocals = null;
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
}

  果真Thread中擁有變量threadLocals,孤陋寡聞啊,各位老鐵,咋們繼續往下看代碼,判斷一下從線程中獲取到的ThreadLocalMap,若是不爲空則,調用ThreadLocalMap類的set方法保存一下,,若是爲空,則new一個ThreadLocalMap對象出來 這裏涉及到了ThreadLocalMap這個類,咱們來詳細的看一下這個類

2.3 ThreadLocalMap類

2.3.1 構造函數

 1 static class Entry extends WeakReference<ThreadLocal<?>> {
 2              /** The value associated with this ThreadLocal. */
 3             Object value;
 4 
 5              Entry(ThreadLocal<?> k, Object v) {
 6                  super(k);
 7                  value = v;
 8              }
 9          }
10  
11          /**
12           * The initial capacity -- MUST be a power of two.
13           */
14          private static final int INITIAL_CAPACITY = 16;
15  
16          /**
17          * The table, resized as necessary.
18           * table.length MUST always be a power of two.
19           */
20          private Entry[] table;
21  
22          /**
23           * The number of entries in the table.
24          */
25          private int size = 0;
26  
27          /**
28           * The next size value at which to resize.
29           */
30          private int threshold; // Default to 0
31  
32  ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
33              table = new Entry[INITIAL_CAPACITY];
34              int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
35              table[i] = new Entry(firstKey, firstValue);
36              size = 1;
37              setThreshold(INITIAL_CAPACITY);
38  }
39 
40 /**
41          * Set the resize threshold to maintain at worst a 2/3 load factor.
42          */
43         private void setThreshold(int len) {
44             threshold = len * 2 / 3;
45         }
46  /**
47          * The next size value at which to resize.
48          */
49         private int threshold; // Default to 0

        第32-33行 :  咱們看到ThreadLocalMap是ThreadLocal的匿名內部類,且構造方法也就是將該ThreadLocal實例做爲key,要保持的對象做爲值。變量table用來用來存放存放數據,咱們能夠看到table數組的初始大小是INITIAL_CAPACITY = 16  英文註釋是「初始容量——必須是2的冪。」   這裏咱們又碰到了「2的冪」關鍵字了,咱們繼續往下看

     第34行: int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);  這行代碼徹底有些看不懂 ,爲了避免理解流程,咱們能夠先不用懂,就模糊的理解爲經過ThreadLoad的threadLocalHashCode變量哈希碼來生成一個下標位置,繼續往下看

  第35 - 49行:將ThreadLocal對象和value值保存到Entry對象中再保存到table數組中,設置初始的size,設置threshold閾值,這個閾值適用於擴容,當發現存入的數據大小打到了當前長度size的二分之三,就會觸發擴容,將當前table數組的大小擴充到原來的兩倍。

  而後咱們能夠看到Entry對象中對咱們的ThreadLocal參數是採用弱引用的,這點對咱們後續分析ThreadLocal內存泄漏這款有所幫助,先提醒一下你們。

  而後咱們全面來看一下int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);   這行代碼所帶來的的意思,咱們再和上面有可能有聯繫的一些代碼所有給粘貼在一塊兒

private static final int HASH_INCREMENT = 0x61c88647;
private static AtomicInteger nextHashCode = new AtomicInteger();
private final int threadLocalHashCode = nextHashCode();


int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
 /**
         * The initial capacity -- MUST be a power of two.
         */
private static final int INITIAL_CAPACITY = 16;

  在看代碼以前咱們先來了解一個小的知識點,看過HashMap源碼的同窗必定知道「哈希衝突」這個問題

咱們使用同一個哈希函數來計算不止一個的待存放的數據在表中的存放位置,
老是會有一些數據經過這個轉換函數計算出來的存放位置是相同的,這就是哈希衝突。
也就是說,不一樣的關鍵字經過同一哈希轉換函數計算出相同的哈希地址。

  經過ThreadLocal和ThreadLocalMap的源碼能夠知道,裏面存值table的下標是經過ThreadLocal的哈希碼生成的,那麼在ThreadLocalMap一樣的存在這個哈希衝突問題,那咱們來看看ThreadLocal是怎麼來解決這個問題的呢?

  咱們知道ThreadLocalMap的初始長度爲16,每次擴容都增加爲原來的2倍,即它的長度始終是2的n次方,大小必須是2的N次方呀(len = 2^N),那 len-1 的二進制表示就是低位連續的N個1,以16爲例,16-1的二進制是15(十進制) = 1111(二進制),而 key.threadLocalHashCode & (len-1) 的值就是 threadLocalHashCode 的低N位(&運算符我就不給你們進行解釋了,你們本身百度一下位運算符),而這樣作的目的是能均勻的產生哈希碼的分佈,我一臉懵逼,那讓咱們來看一下

public static  int HASH_INCREMENT = 0x61c88647 ;

public static void range(int value){
        for (int i = 0; i < value; i++) {
            int nextHashCode = i*HASH_INCREMENT + HASH_INCREMENT;
            System.out.println((nextHashCode & (value - 1))+",");
        }
    }

range(16);



//輸出結果
7 14 5 12 3 10 1 8 15 6 13 4 11 2 9 0

  臥槽,真的能夠均勻的產生,難道0x61c88647這個數值這麼神奇?下面是網上搜到的關於這個數字的節選,我反正是看不懂,仍是給你們貼出來吧

①這個魔數的選取與斐波那契散列有關,0x61c88647對應的十進制爲1640531527。
②斐波那契散列的乘數能夠用(long) ((1L << 31) * (Math.sqrt(5) - 1))能夠獲得2654435769,若是把這個值給轉爲帶符號的int,則會獲得-1640531527。
換句話說 (1L << 32) - (long) ((1L << 31) * (Math.sqrt(5) - 1))獲得的結果就是
1640531527也就是0x61c88647 。
③經過理論與實踐,當咱們用0x61c88647做爲魔數累加爲每一個ThreadLocal分配各自的ID也就是threadLocalHashCode再與2的冪取模,獲得的結果分佈很均勻。

  理解了生成方式咱們繼續往下看ThreadLocalMap的源碼吧

2.3.2 set函數

 1  private void set(ThreadLocal<?> key, Object value) {
 2 
 3             Entry[] tab = table;
 4             int len = tab.length;
 5             int i = key.threadLocalHashCode & (len-1);
 6 
 7             for (Entry e = tab[i];
 8                  e != null;
 9                  e = tab[i = nextIndex(i, len)]) {
10                 ThreadLocal<?> k = e.get();
11 
12                 if (k == key) {
13                     e.value = value;
14                     return;
15                 }
16 
17                 if (k == null) {
18                     replaceStaleEntry(key, value, i);
19                     return;
20                 }
21             }
22 
23             tab[i] = new Entry(key, value);
24             int sz = ++size;
25             if (!cleanSomeSlots(i, sz) && sz >= threshold)
26                 rehash();
27         }

  set方法也很簡單,就是去table中獲取第i位的數據,若是發現當前第i爲的ThreadLocal等於當前傳入的ThreadLocal,就更新i位的value,若是發現第i爲的ThreadLocal爲空,因爲咱們的Entry對ThreadLocal是弱引用,就表示以前保存的ThreadLocal已經被回收了,replaceStaleEntry()方法就不和你們細看了,就是首先清理掉空的Entry,而後將後面的 Entry 進行 rehash 填補空洞,25-27行就是對閾值的判斷,若是超過了就進行擴容。

2.3.3 getEntry函數

 1  private Entry getEntry(ThreadLocal<?> key) {
 2             int i = key.threadLocalHashCode & (table.length - 1);
 3             Entry e = table[i];
 4             if (e != null && e.get() == key)
 5                 return e;
 6             else
 7                 return getEntryAfterMiss(key, i, e);
 8 }
 9 
10  private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
11             Entry[] tab = table;
12             int len = tab.length;
13 
14             while (e != null) {
15                 ThreadLocal<?> k = e.get();
16                 if (k == key)
17                     return e;
18                 if (k == null)
19                     expungeStaleEntry(i);
20                 else
21                     i = nextIndex(i, len);
22                 e = tab[i];
23             }
24             return null;
25         }

  

1 private static int nextIndex(int i, int len) {
2     return ((i + 1 < len) ? i + 1 : 0);
3 }

  getEntry函數也很簡單,使用位運算找到哈希槽。若哈希槽中爲空或 key 不是當前 ThreadLocal 對象則會調用getEntryAfterMiss方法,能夠看到getEntryAfterMiss 方法會循環查找直到找到或遍歷全部可能的哈希槽, 在循環過程當中可能遇到4種狀況:

①哈希槽中是當前ThreadLocal, 說明找到了目標 
②哈希槽中爲其它ThreadLocal, 須要繼續查找 
③哈希槽中爲null, 說明搜索結束未找到目標 
④哈希槽中存在Entry, 可是 Entry 中沒有 ThreadLocal 對象。由於 Entry 使用弱引用, 這種狀況說明 ThreadLocal 被GC回收。 爲了處理GC形成的空洞(stale entry), 須要調用expungeStaleEntry方法進行清理。

 2.3.3 remove函數

 1  private void remove(ThreadLocal<?> key) {
 2             Entry[] tab = table;
 3             int len = tab.length;
 4             int i = key.threadLocalHashCode & (len-1);
 5             for (Entry e = tab[i];
 6                  e != null;
 7                  e = tab[i = nextIndex(i, len)]) {
 8                 if (e.get() == key) {
 9                     e.clear();
10                     expungeStaleEntry(i);
11                     return;
12                 }
13             }
14         }

  remove方法和get方法相似,也是找出對應的Entry對象,而後調用其clean方法清理

  

  ok,這裏咱們就把ThreadLocalMap的源碼看完了,其實相似ThreadLocal的源碼同樣,來咱們繼續往下看

2.4  get函數

public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }

 private T setInitialValue() {
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
        return value;
    }

protected T initialValue() {
    return null;
}

   都很簡單,首先去獲取當前線程的ThreadLocalMap中的Entry對象,若是不爲空直接返回Entry的value值,若是爲空,則調用initialValue方法,這個方法能夠被重寫 ,默認返回爲null,將當前線程的ThreadLocal對象保存在ThreadLocalMap中,返回上層null。

2.5 remove函數

 public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }

  也很簡單,這裏就不在囉嗦了  ,ok,到這裏咱們已經看完了ThreadLocal源碼 ,真正的功能都是在ThreadLocalMap中進行操做的,這裏我有一個疑問,爲何不適用HashMap來替代ThreadLocalMap呢?如一下代碼:

class ThreadLocal { 
  private Map values = Collections.synchronizedMap(new HashMap());
 
  public Object get() {
    Thread curThread = Thread.currentThread();
    Object o = values.get(curThread);
    if (o == null && !values.containsKey(curThread)) {
      o = initialValue();
      values.put(curThread, o);
    }
    return o;
  }
 
  public void set(Object newValue) {
    values.put(Thread.currentThread(), newValue);
  }
}

    這樣貌似也沒問題啊,乍一看的確沒毛病,可是咱們知道ThreadLocal本意是避免併發,用一個全局Map顯然違背了這一初衷,且會致使內存泄漏,用Thread當key,除非手動調用remove,不然即便線程退出了會致使:1)該Thread對象沒法回收;2)該線程在全部ThreadLocal中對應的value也沒法回收。

  這時候會有同窗提出疑問了,你使用ThreadLocalMap的話就不會致使內存泄漏嗎?

  不少人認爲:threadlocal裏面使用了一個存在弱引用的map,當釋放掉threadlocal的強引用之後,map裏面的value卻沒有被回收.而這塊value永遠不會被訪問到了。在這種狀況下咱們的確會存在value的泄漏。

  咱們看上圖,每一個thread中都存在一個map, map的類型是ThreadLocal.ThreadLocalMap. Map中的key爲一個threadlocal實例. 這個Map的確使用了弱引用,不過弱引用只是針對key. 每一個key都弱引用指向threadlocal. 當把threadlocal實例置爲null之後,沒有任何強引用指向threadlocal實例,因此threadlocal將會被gc回收, 可是,咱們的value卻不能回收,由於存在一條從current thread鏈接過來的強引用. 只有當前thread結束之後, current thread就不會存在棧中,強引用斷開, Current Thread, Map, value將所有被GC回收。

  咱們從以前的ThreadLocalMap源碼能夠知道,弱引用只存在於key上,因此key會被回收,但當線程還在運行的狀況下,value仍是在被線程強引用而沒法釋放,只有當線程結束以後或者咱們調用set、get方法的時候回去移除已被回收的Entry( replaceStaleEntry這個方法),給出的建議是,當ThreadLocal使用完成的時候,調用remove方法將value移除掉,這樣就不會存在內存泄漏了。

 

3,總結

  咱們代碼看完了,須要對ThreadLocal的使用場景進行總結一下 :

  ThreadLocal爲每個線程都提供了變量的副本,使得每一個線程在某一時間訪問到的並非同一個對象,這樣就隔離了多個線程對數據的數據共享。

  ok,感受很久都沒有些博客了,思路和語言表達都有些生疏,爭取後面一直堅持寫一寫,加油

相關文章
相關標籤/搜索