Java8 ThreadLocal 源码分析
可參考文章: Java8 IdentityhashMap 源碼分析
IdentityhashMap 與 ThreadLocalMap 一樣都是采用線性探測法解決哈希沖突,有興趣的可以先了解下 IdentityhashMap 。
一、ThreadLocal 簡介
在學習源碼之前,有一個概念我們需要先明白:ThreadLocal 可以使多線程間數據讀寫隔離,因此 ThreadLocal 解決的是線程局部變量安全性問題,并不是多線程間共享變量安全性問題。
ThreadLocal 在使用時必須先初始化 value,否則會報空指針異常,你可以通過 set 方法與重寫 initialValue 方法兩種方式初始化 value。
下面是 ThreadLocal 原理圖,讀源碼的時候可以參考。
二、ThreadLocal 源碼
我們先來了解一下 ThreadLocal,然后再逐漸了解 ThreadLocalMap。
2.1 內部相關屬性
/** ThreadLocal 的哈希值通過一個原子類計算*/private final int threadLocalHashCode = nextHashCode();/*** 用于計算 ThreadLocal 哈希值的原子類*/private static AtomicInteger nextHashCode = new AtomicInteger();/** * 計算 ThreadLocal 哈希值的魔數 * 該值生成出來的值可以較為均勻地分布在 2 的冪大小的數組中* 據說與斐波那契散列有關...*/private static final int HASH_INCREMENT = 0x61c88647;ThreadLocalMap 的結構是通過純數組實現的,因此 ThreadLocal 計算哈希值的方式也比較特殊,通過 nextHashCode() 方法生成哈希值,下面是具體實現。
private static int nextHashCode() {return nextHashCode.getAndAdd(HASH_INCREMENT);}生成哈希值時每次加上 0x61c88647,據了解通過 0x61c88647 計算出來的哈希值能夠均勻的分布在 2 的冪大小的數組中,有興趣的可以網上查一下進行詳細的了解。
2.2 set 方法
public void set(T value) {Thread t = Thread.currentThread();// 根據當前線程獲取對應的 mapThreadLocalMap map = getMap(t);if (map != null)// key 是當前 ThreadLocal 對象的引用map.set(this, value);elsecreateMap(t, value);}在設置 value 時會先調用 getMap 方法根據當前線程獲取對應的 map,如果 map 存在就設置值,不存在則創建 map,下面跟別來看下對應的方法(map.set 方法會在下面分析)。
ThreadLocalMap getMap(Thread t) {return t.threadLocals;}getMap 方法很簡單,就是返回當前線程的 threadLocals,這個 threadLocals 就是 ThreadLocalMap 對象。由此可以知道每個 Thread 內部都有一個 ThreadLocalMap 變量。
void createMap(Thread t, T firstValue) {t.threadLocals = new ThreadLocalMap(this, firstValue);}createMap 方法也比較簡單,創建一個 ThreadLocalMap 并賦值給當前線程的 threadLocals 變量。
2.3 get 方法
public T get() {Thread t = Thread.currentThread();// 根據當前線程獲取對應的 mapThreadLocalMap map = getMap(t);if (map != null) {// 根據當前對象獲取到對應的 Entry,getEntry 方法會在下面 ThreadLocalMap 中看到ThreadLocalMap.Entry e = map.getEntry(this);if (e != null) {@SuppressWarnings("unchecked")T result = (T)e.value;// 返回 Entry 中對應的 valuereturn result;}}// map 為空時創建return setInitialValue();}如果 map 存在的話會先獲取到當前線程對應的 map,然后根據當前 ThreadLocal 的弱引用獲取 Entry,最終返回 Entry 中的 value 即可。如果 map 不存在則調用 setInitialValue 方法創建,下面是具體實現細節。
private T setInitialValue() {// 獲取 initialValue() 方法中對應的 value,// 如果沒有重寫 initialValue 方法會拋空指針異常T value = initialValue();Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);// 如果對應的 map 不為空,則重置對應的 valueif (map != null)map.set(this, value);// map 為空,初始化 mapelsecreateMap(t, value);return value;}2.4 remove 方法
public void remove() {ThreadLocalMap m = getMap(Thread.currentThread());if (m != null)m.remove(this);}remove 方法調用了 ThreadLocalMap 中的 remove 方法刪除當前線程的,這個方法到下面介紹 ThreadLocalMap 時再詳細分析。
三、ThreadLocalMap 源碼分析
ThreadLocal 源碼中最有意思的就屬 ThreadLocalMap 了,它到底有哪些巧妙的設計呢?下面就來一探究竟吧。
3.1 內部相關屬性
/*** 哈比表數組默認初始化大小*/private static final int INITIAL_CAPACITY = 16;/*** 底層哈希表數組*/private Entry[] table;/*** 哈希表鍵值對個數*/private int size = 0;/*** 擴容閾值*/private int threshold; // Default to 0/*** 設置擴容閾值為容量的 2/3*/private void setThreshold(int len) {threshold = len * 2 / 3;}/*** Increment i modulo len.當到數組尾時會從頭開始*/private static int nextIndex(int i, int len) {return ((i + 1 < len) ? i + 1 : 0);}/*** Decrement i modulo len.當到數組頭部時會從尾部開始*/private static int prevIndex(int i, int len) {return ((i - 1 >= 0) ? i - 1 : len - 1);}ThreadLocalMap 與 HashMap 最大的不同是當發生哈希沖突時不通過鏈表形式來解決沖突,而是使用線性探測法解決哈希沖突。ThreadLocalMap 的擴容閾值是 2/3,與 IdentityHashMap 一致,有興趣的可以看下 IdentityHashMap,它們兩個的結構是很相似的。
3.2 構造函數
我們來看其中一個構造函數。
/*** 第一次添加的時候會調用該構造函數進行初始化,并設置第一個線程對應的 key 與 value*/ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {// 初始化哈希表數組table = new Entry[INITIAL_CAPACITY];// 計算桶位置,這個哈希值的計算在上面我們解釋過int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);// 設置到對應的桶位置上(已經有了一個 key 與 value)table[i] = new Entry(firstKey, firstValue);// 初始化 size 為 1size = 1;// 設置擴容閾值為初始容量的 2/3setThreshold(INITIAL_CAPACITY);}當設置擴容閾值時調用了 setThreshold 方法,這個方法很簡單,就是把閾值設置為數組長度的 2/3。
private void setThreshold(int len) {threshold = len * 2 / 3;}3.3 Entry 結構
static class Entry extends WeakReference<ThreadLocal<?>> {/** The value associated with this ThreadLocal. */Object value;Entry(ThreadLocal<?> k, Object v) {// key 為弱引用super(k);value = v;}}ThreadLocalMap 中存儲鍵值對的結構是 Entry,Entry 實現了 WeakReference 類使 key 成為一個弱引用。Java 語言的弱引用對象意味著只要被垃圾收集器線程掃描到,那么不管當前內存是否足夠都會被回收。關于強引用、軟引用、弱引用與虛引用的差別可以查閱資料進行詳細了解。
3.4 set 方法
ThreadLocalMap 添加鍵值對的方法不是 put 而是 set,如下:
private void set(ThreadLocal<?> key, Object value) {// 獲取哈希表數組 Entry[] tab = table;int len = tab.length;// 計算 key 對應的桶位置int i = key.threadLocalHashCode & (len-1);// e != null 意味著哈希沖突或是 key 重復// e = tab[i = nextIndex(i, len)] 線性探測法解決哈希沖突for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {// 獲取 key 的引用ThreadLocal<?> k = e.get();// key 重復,value 覆蓋if (k == key) {e.value = value;return;}// entry 不為 null,key 為 null,是因為 key 是弱引用,可能已經被 GC 回收了if (k == null) {replaceStaleEntry(key, value, i);return;}}// 找到插入的位置,存儲 key 與 valuetab[i] = new Entry(key, value);int sz = ++size;// cleanSomeSlots 用于刪除可能已經被 GC 回收的 key// 如果沒有 key 被 GC 回收,并且哈希表數組中的鍵值對數量大于 2/3,執行擴容操作if (!cleanSomeSlots(i, sz) && sz >= threshold)rehash();}當插入鍵值對的時候,先根據哈希值計算出在哈希表數組中的位置,如果當前桶位置上的 entry 不為空,意味著出現哈希沖突或者是 key 重復。key 重復時直接將原來的 value 覆蓋即可,上面我們已經提到了如果發生哈希沖突,ThreadLocalMap 通過線性探測法方式解決,因此需要繼續從數組當前位置向后查找可插入位置(nextIndex)。當插入鍵值對過后會判斷是否需要對哈希表數組擴容,整體的流程還是很清晰的。下面是 nextIndex 的具體實現:
private static int nextIndex(int i, int len) {return ((i + 1 < len) ? i + 1 : 0);}當查找到數組尾部時,如果還沒有找到要插入的位置,會從頭繼續查找,因此可以把哈希表數組理解為一個環狀的結構。
ThreadLocalMap 的 key 因為是弱引用,因此當發生哈希沖突時,沖突的 entry 可能不為 null,而 key 為 null(弱引用被 GC 回收),如果 key 為 null 則調用 replaceStaleEntry 方法,下面就來看一下這個方法:
private void replaceStaleEntry(ThreadLocal<?> key, Object value,int staleSlot) {// 獲取哈希表數組Entry[] tab = table;int len = tab.length;Entry e;// 記錄 key 被擦除的桶位置(為 staleSlot 位置前的第一個連續的 key 被擦除的索引// 或 staleSlot 位置后第一個連續的 key 被擦除或 key 重復的索引)int slotToExpunge = staleSlot;// 尋找 staleSlot 索引前連續不為 null 的 key 被擦除的桶位置// 注意循環結束的條件是 e == null 與 IdentityHashMap 相同,也是線性探測法解決哈希沖突的截止條件,有興趣的可以看下 IdentityHasHMapfor (int i = prevIndex(staleSlot, len); (e = tab[i]) != null; i = prevIndex(i, len))if (e.get() == null)slotToExpunge = i;// 向后查找for (int i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) {ThreadLocal<?> k = e.get();// key 重復if (k == key) {e.value = value;// i 位置與 staleSlot 位置的 entry 互換,因為 staleSlot 位置上的 key 已經被回收,沒有意義了// TODO 那為什么不把 key 被 GC 回收的 entry 置為 null 而是位置互換呢?不要急,下面 expungeStaleEntry 方法會做tab[i] = tab[staleSlot];tab[staleSlot] = e;// Start expunge at preceding stale entry if it exists// slotToExpunge == staleSlot 意味著向前沒有查找到連續的鍵值對 key 被擦除的情況if (slotToExpunge == staleSlot)slotToExpunge = i;// 將 slotToExpunge 位置上的 entry 清除cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);return;}// 如果 i 位置上的 key 也已經被擦除將 slotToExpunge 置為 iif (k == null && slotToExpunge == staleSlot)slotToExpunge = i;}// If key not found, put new entry in stale slot// 把新的鍵值對直接存儲在 staleSlot 位置tab[staleSlot].value = null;tab[staleSlot] = new Entry(key, value);// If there are any other stale entries in run, expunge them// 如果向前或向后找到了 key 被擦除的 entry,則清除 slotToExpunge 位置上的鍵值對if (slotToExpunge != staleSlot)cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);}replaceStaleEntry 方法相對來說比較難以理解,這里總結下我的思路過程,如果大家覺得哪里不對,可以在下面留言。首先我們先確定下 replaceStaleEntry 方法中的 staleSlot 字段,它表示新增鍵值對時 key 重復且 key 被 GC 回收情況下在哈希表數組中的位置。
replaceStaleEntry 方法先從 staleSlot 位置向前查找 entry 不為 null,key 為 null 的 鍵值對,記錄在哈希表數組中的位置,注意這里循環結束的條件是 (e = tab[i]) != null,只要 entry 為 null 就停止循環,這個是線形探測法解決哈希沖突的重要判斷條件,在 IdentityHashMap 中也有體現。
向前查找過后開始向后查找,結束的條件與之前一致,只不過向后查找可能會出現 key 相同的情況,如果 key 重復則重置其 value,然后把 staleSlot 位置與 i 位置的鍵值對位置互換,為什么要互換呢?原因是 staleSlot 位置上的 entry 的 key 已經被 GC 回收了,為了保證哈希沖突的所有鍵值對連續,因此需要把后面沖突的鍵值對前移。
接下來看這一段代碼:
if (slotToExpunge == staleSlot)slotToExpunge = i;// 將 slotToExpunge 位置上的 entry 清除cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);slotToExpunge == staleSlot 表示向前沒有查找到連續的鍵值對 key 被擦除的情況,把 slotToExpunge 的值置為了 i,然后執行了 cleanSomeSlots(expungeStaleEntry(slotToExpunge), len),這個 slotToExpunge 在這里表示鍵值對交換過之后 key 被 GC 回收的那個 entry 所在哈希表數組中索引的位置。因為它的 key 已經被 GC 回收了,就意味著這個鍵值對沒有存在的必要了,需要對其清除,于是就執行了 expungeStaleEntry 方法:
private int expungeStaleEntry(int staleSlot) {Entry[] tab = table;int len = tab.length;// value 置 null,對應桶位置上的 Entry 也置 nulltab[staleSlot].value = null;tab[staleSlot] = null;// 鍵值對數量減 1size--;Entry e;int i;// 刪除一個 key 被擦除的鍵值對,可能因為之前哈希沖突,導致后面桶位置上的鍵值對位置不準確,因此要向前調整后面桶位置上的鍵值對// 從 staleSlot 位置向后遍歷,要求必須連續,與 IdentityHashMap 一致 for (i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) {ThreadLocal<?> k = e.get();// 如果后面桶位置上鍵值對被擦除,則直接清除,因此 expungeStaleEntry 方法并不是只清除 staleSlot 位置上的鍵值對if (k == null) {e.value = null;tab[i] = null;size--;} else {// 并不是向前移動,而是重新 rehash,計算對應的桶位置// TODO 重點理解,重新 rehash 解決之前可能存在哈希沖突的情況int h = k.threadLocalHashCode & (len - 1);if (h != i) {tab[i] = null;// Unlike Knuth 6.4 Algorithm R, we must scan until// null because multiple entries could have been stale.while (tab[h] != null)h = nextIndex(h, len);tab[h] = e;}}}// 返回 staleSlot 之后第一個鍵值對為 null 的桶位置return i;}expungeStaleEntry 中上來就對 staleSlot 位置上的鍵值對置 null,然后鍵值對的數量減一,但是并不說刪除一個鍵值對這里就結束了。我們說過 ThreadLocalMap 是通過線性探測法來解決哈希沖突的,當刪除一個鍵值對之后需要從當前刪除的位置向后循環,判斷后面是否存在因為哈希沖突被移動到后面去的鍵值對,如果有就重新計算其哈希值,然后存儲到對應的位置上,當然重新計算哈希值也要考慮哈希沖突。順便在這里提一下,這里與 IdentityhashMap 的處理方式是不同的,IdentitiHashMap 并不會重新計算后面沖突的 key 的哈希值而是采取向前移動的方式來解決。
到這里還不算完,expungeStaleEntry 方法中返回了一個 i,這個 i 表示 staleSlot 位置后第一個 key 被 GC 回收的數組索引位置。執行完 expungeStaleEntry 方法后根據其返回值又執行了 cleanSomeSlotsc 方法,這個方法又是干嘛的呢?下面來簡單的分析一下:
private boolean cleanSomeSlots(int i, int n) {boolean removed = false;Entry[] tab = table;int len = tab.length;do {i = nextIndex(i, len);// 獲取對應桶位置上的 EntryEntry e = tab[i];// Entry 不為 null,key 為 null 是因為 key 是弱引用可能會被 GC 回收,因此需要在哈希表中刪除if (e != null && e.get() == null) {n = len;// 如果有鍵值對被擦出就返回 trueremoved = true;// 刪除 i 位置上的鍵值對i = expungeStaleEntry(i);}} /* 對數掃描,并不會掃描整個哈希表數組 */while ( (n >>>= 1) != 0);return removed;}根據 cleanSomeSlots 的方法名我們應該可以知道這個方法大概做了什么,清除一些哈希槽位置上的鍵值對。這個方法會循環向后判斷當前桶位置上的 key 是否被 GC 回收了,如果被回收了就調用 expungeStaleEntry 方法清除其鍵值對。注意這里不是一直向后循環,而是采取對數的方式,這就說明,整個循環下來并不會清除所有 key 被 GC 回收的鍵值對,會存在一些漏網之魚。
關于 set 方法就簡單的分析到這里,其中還有一些細節大家有興趣可以自己查看,如果哪里有錯誤的地方大家可以在下面留言交流。
3.5 set 方法之 rehash
我們上面一直在分析哈希沖突的情況,還有一個比較重要的 rehash 過程,添加過鍵值對后判斷是否需要 rehash 的是下面這段代碼:
// !cleanSomeSlots(i, sz) 表示沒有鍵值對因為 key 被回收而清除// sz >= threshold 表示哈希表數組中的鍵值對數量已經大于了擴容閾值if (!cleanSomeSlots(i, sz) && sz >= threshold)rehash();當判斷條件通過后會調用 rehash 方法。
private void rehash() {// 清除所有 key 被擦出的鍵值對expungeStaleEntries();// Use lower threshold for doubling to avoid hysteresis// 再次判斷// TODO Q:這個判斷有什么作用?不可能是 false 的啊// A:因為上面調用了 expungeStaleEntries 方法,可能有的鍵值對被移除導致哈希表數組的鍵值對非常少,此時就沒有擴容的必要了if (size >= threshold - threshold / 4)resize();}rehash() 方法先調用了 expungeStaleEntries() 方法,這個方法里會循環整個哈希表數組,然后清除所有的 key 被 GC 回收的鍵值對。
private void expungeStaleEntries() {Entry[] tab = table;int len = tab.length;for (int j = 0; j < len; j++) {Entry e = tab[j];if (e != null && e.get() == null)expungeStaleEntry(j);}}為什么上面已經執行過了 cleanSomeSlots 方法來清除鍵值對,為什么這里又要判斷一次呢?原因就是 cleanSomeSlots 方法并不會循環整個哈希表,會存在一些漏網之魚,而 expungeStaleEntries() 方法會連那些漏網之魚一起處理掉。
調用了 expungeStaleEntries() 方法之后,需要重新判斷鍵值對數量,只有當條件滿足時才會調用 resize() 方法。下面是 resize() 方法的是實現:
private void resize() {Entry[] oldTab = table;int oldLen = oldTab.length;// 新哈希表的大小為原哈希表大小的 2 倍int newLen = oldLen * 2;// 初始化新哈希表Entry[] newTab = new Entry[newLen];// 記錄新哈希表中鍵值對的個數int count = 0;// 遍歷老哈希表數組,進行 rehashfor (int j = 0; j < oldLen; ++j) {// 獲取老哈希表桶位置上的 EntryEntry e = oldTab[j];if (e != null) {ThreadLocal<?> k = e.get();// 如果 key 被回收,則把 value 也置 null// 無時不刻判斷著 key 被擦除的情況if (k == null) {e.value = null; // Help the GC} else {// 計算老哈希表中的鍵值對在新哈希表中的桶位置int h = k.threadLocalHashCode & (newLen - 1);// 這里也可能會產生哈希沖突while (newTab[h] != null)h = nextIndex(h, newLen);newTab[h] = e;count++;}}}// 設置新的擴容閾值,2/3setThreshold(newLen);size = count;// 新的哈希表替代老的哈希表table = newTab;}rehash 的過程其實是比較簡單的,生成新的哈希表,然后遍歷舊的哈希表數組,將鍵值對重新 rehash 存儲到新的哈希表數組中即可。
3.6 getEntry 方法
private Entry getEntry(ThreadLocal<?> key) {// 獲取桶位置int i = key.threadLocalHashCode & (table.length - 1);// 獲取桶位置上對應的鏈表Entry e = table[i];// 哈希不沖突,直接獲取對應的 value 并返回if (e != null && e.get() == key)return e;// 哈希沖突,則遍歷后面的桶位置,進行查找,當然 key 可能因為是弱引用被擦出,需要額外處理elsereturn getEntryAfterMiss(key, i, e);}getEntry 方法實現很簡單,根據 key 的哈希值計算在哈希表數組中的桶位置,然后,如果當前對應的桶位置上的 key 是同一個則直接返回 Entry,反之則調用 getEntryAfterMiss 方法來處理哈希沖突的情況。
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {Entry[] tab = table;int len = tab.length;// Entry 為 null 作為循環結束條件while (e != null) {ThreadLocal<?> k = e.get();// 找到直接返回, valueif (k == key)return e;// 如果 key 被擦出,則清除if (k == null)expungeStaleEntry(i);else// 重置 i 用于循環遍歷i = nextIndex(i, len);e = tab[i];}// 沒有找到返回 nullreturn null;}代碼邏輯很清晰,這里就不進行詳細總結了。
3.7 remove 方法
上面我們看了添加方法,下面來看一下刪除操作:
private void remove(ThreadLocal<?> key) {Entry[] tab = table;int len = tab.length;// 計算出對應的桶位置,當然對應桶位置上的鍵值對并不一定是當前 key 對應的鍵值對,因為可能存在哈希沖突int i = key.threadLocalHashCode & (len-1);// 從 i 位置向后遍歷,遍歷結束的位置是后續桶位置上為 nullfor (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {if (e.get() == key) {// key 清空e.clear();// 調用 expungeStaleEntry 方法expungeStaleEntry(i);return;}}}根據 key 就算哈希值,在哈希表數組中找到對應的位置開始循環判斷,如果 key 相同則調用 expungeStaleEntry 方法直接清除鍵值對。PS:注意循環結束條件 e != null。
關于 ThreadLocal 的源碼就分析到這里,一千個人眼里有一千個哈姆雷特,只有自己去看了才能有更深刻的了解。
關于 jdk1.8 更多的源碼分析,請點擊這里前往:jdk1.8 源碼閱讀
總結
以上是生活随笔為你收集整理的Java8 ThreadLocal 源码分析的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 徐子韩老婆(徐子韩)
- 下一篇: lol端游双倍经验卡怎么用(lol双倍经