總覽
ThreadLocal提供了線程局部變量的解決方案。
我們知道成員變量在多線程下是不安全的,而局部變量的生命週期又取決於變量定義的範圍。那麼有沒有一個變量定義可以專屬於各自線程,生命週期由線程控制,並且和其他線程的變量完全隔離,確保線程安全。簡單想到一個解決辦法,定義一個這樣的結構:Map<Thread, Map<Object, Object>>,Thread爲Key,一個Map爲value,這個Map裏存着這個線程的獨有變量,只需要保證訪問變量的時候都是通過Thread來檢索的,可以從表面上做到線程隔離的效果。結構類似如下下圖:
帶着這個結構思路,再繼續仔細研究一下ThreadLocal是如何實現的。
從ThreadLocal public method中總覽一下它在實現,除了構造方法,以下爲關鍵方法:
set(T value)
,往ThreadLocalMap
放值,key是ThreadLocal本身get()
,從ThreadLocalMap
以ThreadLocal本身爲key獲取Map中的valueremove()
,從ThreadLocalMap
以ThreadLocal本身爲key刪除這個鍵值對
發現所謂的線程局部變量實際都是由一個Map來維護的,並且每個Thread自己持有一個Map,而對於每個Thread持有的Map數據的訪問或操作都只能通過調用ThreadLocal的方法來達成。
整體的數據結構如下圖:
暫且不管那個ThreadLocalMap的結構,從圖中直觀的可以看到每一個Thread持有一個私有Map的結構,和前面想的方案的數據結構對比一下,其實就是把Thread的Key去掉了,直接從Thread裏就可以找到自己存放數據的Map了。
前面已經介紹過,所有線程變量都是通過ThreadLocal來操作的,我們來看下ThreadLocal的使用:
public class StaticThreadLocalTest {
private static final ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
private static Integer num = null;
public static void main(String[] args) {
MyTask task = new MyTask();
new Thread( task ).start();
new Thread( task ).start();
}
static class MyTask implements Runnable {
@Override
public void run() {
// 每個線程隨機生產一個數
Integer count = new Random().nextInt(100);
System.out.println(Thread.currentThread().getName() + ", count: " + count );
// 模擬業務耗時
try{
Thread.sleep(1000);
}catch (Exception e) {
}
// 存儲數據
num = count;
threadLocal.set(count);
// 獲取數據
System.out.println( Thread.currentThread().getName() + ", num: " + num + ", threadLocal: " +threadLocal.get() );
// 模擬業務耗時
try{
Thread.sleep(100);
}catch (Exception e) {
}
// 移除當前線程所存的數據
threadLocal.remove();
}
}
}
以上例子是在網上代碼進行了一些改造,這裏對比num
和threadLocal
的輸出值,來感受ThreadLocal的實際作用。
輸出內容:
Thread-1, count: 82
Thread-0, count: 31
Thread-1, num: 82, threadLocal: 82
Thread-0, num: 82, threadLocal: 31
多次執行發現num值會是和Thread對應的count不一樣而threadLocal值是和count一致。
debug代碼輸出:
Thread-1, count: 35
Thread-0, count: 59
Thread-1, num: 35, threadLocal: 35
Thread-0, num: 59, threadLocal: 59
也可以達到num和ThreadLocal都和對應Thread的count一致。
由此可以感知到,ThreadLocal是保證了變量數據是線程隔離的。而在實際應用中,當使用的每個線程都需要用到一個對象,那麼就可以把這個對象的副本放到ThradLocal中,通過這個ThreadLocal操作對象的時候,其實是對Thread獨有的副本進行操作,互不干擾。
核心結構分析
從前面的使用代碼可以看出,定義一個ThradLocal可以提供給多個線程使用,線程通過ThradLocal的方法來存取屬於自己的數據集,而每個線程也是可以使用多個ThradLocal的。回顧數據結構圖,那個屬於Thread的Map是核心所在,接下去將花費大量篇幅對這個Map進行全面細緻的分析,其中會涉及到大量關聯知識點,都會盡可能的展開深究,以求展示完整脈絡。
熟悉HashMap結構的朋友應該知道,它是以數組+鏈表+紅黑樹實現的,後續的分析中可以參照HashMap進行比對學習。
ThreadLocalMap的最小單元是Entry,一個Entry存着Key和Value,Key始終是ThreadLocal,Value是使用者存的對象,其中這個Key是弱引用的。
/**
* The entries in this hash map extend WeakReference, using
* its main ref field as the key (which is always a
* ThreadLocal object). Note that null keys (i.e. entry.get()
* == null) mean that the key is no longer referenced, so the
* entry can be expunged from table. Such entries are referred to
* as "stale entries" in the code that follows.
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
註釋中特別對這個弱引用做了特別的解釋,這是ThradLocal中非常關鍵的設計
- WeakReference是什麼
當一個對象僅僅被weak reference(弱引用)指向, 而沒有任何其他strong reference(強引用)指向的時候, 如果這時GC運行, 那麼這個對象就會被回收,不論當前的內存空間是否足夠,這個對象都會被回收。
-
這個Key是ThreadLocal,所以這個弱引用需要產生作用,取決於ThreadLocal是否被強引用着。
-
Map的設計機制中是對這個key是爲null的情況做判斷的,如果是null,就認爲是過期無用的數據,可以丟棄,注意在註釋上的
expunged
這個單詞,後續方法中清理的時候就用這個單詞的方法。
Map中維護着一個Entry
數組:
/**
* The table, resized as necessary.
* table.length MUST always be a power of two.
*/
private Entry[] table;
對於數組,隨着存儲數據逐漸增多,必然需要擴容,這點在HashMap
中也是有的,另外特別要求是數組的長度必須保證是二次冪,這個和Hash算法有關,後續會關聯到。
源碼分析
瞭解好基本的一些信息後,我們深入ThreadLocalMap
的方法源碼。
ThreadLocalMap
set方法:
/**
* Set the value associated with key.
*
* @param key the thread local object
* @param value the value to be set
*/
private void set(ThreadLocal<?> key, Object value) {
// We don't use a fast path as with get() because it is at
// least as common to use set() to create new entries as
// it is to replace existing ones, in which case, a fast
// path would fail more often than not.
// 存儲使用的數組
Entry[] tab = table;
int len = tab.length;
// hash算法 算出數組下標
int i = key.threadLocalHashCode & (len-1);
// 開始從算出的下標開始向後判斷每個槽位有沒有不爲空的
// 如果不爲空,則有三種情況:
// 1,已存儲的Entry的key和set的key相同;
// 2,存儲的Entry的key是null,表示爲過期Entry;
// 3,hash算法計算後和另一個key的hash計算後是同一個數組下標,表示發生hash衝突
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
// 已經有相同key對應的Entry存在,也就找到key存的位置了,直接替換value即可
if (k == key) {
e.value = value;
return;
}
// 另一種情況是存在的Entry內的key是null,表示已被回收,
// 這個就是key弱引用的關鍵機制,在set的時候也有判斷,判斷出來後就調用replaceStaleEntry方法
if (k == null) {
// 用新值替換過期槽上的值,不過還做了其他的事,這個方法後面分析
replaceStaleEntry(key, value, i);
return;
}
// 注意hash衝突的情況就繼續循環數組,往後面繼續找可以set的槽位
}
// 到這裏說明i下標下的槽位是空的,直接new一個Entry放入
tab[i] = new Entry(key, value);
// 更新已經使用的數組數量
int sz = ++size;
// 做一次清理過期槽位(注意這個some),如果沒有清理出一個並且數組使用量已超過擴容閥值,則進行擴容
if (!cleanSomeSlots(i, sz) && sz >= threshold)
// 重新調整數組
rehash();
}
/**
* Re-pack and/or re-size the table. First scan the entire
* table removing stale entries. If this doesn't sufficiently
* shrink the size of the table, double the table size.
*/
private void rehash() {
// 先進行一次全掃描的清理過期槽位
expungeStaleEntries();
// Use lower threshold for doubling to avoid hysteresis
// 爲防止遲滯現象,使用四分之三的閥值和size進行比較來確定是否進行擴容
// 這裏說的遲滯現象意思是這樣的,全數組的掃描清理過期數據只在rehash方法時觸發,
// 所以有可能出現的一種情況是有一些需要被清理的數據一直沒有被清理,
// 而在當判斷出size超過擴容閥值後進入rehash方法進行一次全數組掃描清理,
// 這些沒有被清理的數據在此時會被清理,如果只是少量則可以看作是運行過程中的遲滯效果,
// 所以在判斷是否真正進行擴容的時候,用減小四分之一的閥值去比較。
if (size >= threshold - threshold / 4)
resize();
}
注:遲滯現象
以下四個場景的圖基本描述清楚了set
方法的流程。
set
方法場景一:
set
方法場景二:
set
方法場景三:
set
方法場景四:
解決hash衝突的方法
- 開放地址法 核心思想是當出現衝突的時候,以衝突地址爲基礎,計算出下一個table上的地址,再衝突則重複計算,知道找到空位,ThreadHashMap的計算方式就是往後移一位,當然還有其他的計算方式。
- 拉鍊法 這個也是java選手最熟悉的,因爲大家都瞭解過HashMap的hash衝突時的解決方式就是建立鏈表形式解決,這個就是拉鍊法的核心思想,在原先的table數據結構基礎上再增加另一個數據結構存儲hash衝突的值。
- 再散列法 準備多個hash方法,依次執行直到找到一個空位放入
前面set方法場景四就是hash衝突的場景,這裏採用了開放地址法來解決hash衝突。
疑問:如果在場景四情況下,因爲遍歷是從hash計算後的下標開始往後的,會不會出現往後的槽位都不能放數據的情況呢?
解釋:不會出現這種情況,這裏容易忽視for循環中的nextIndex(i, len)
方法,代碼:return ((i + 1 < len) ? i + 1 : 0);
,可以看到當下標到達len-1,就會將下標轉成數組的頭部,這就保證了循環數組的過程是頭尾相連的,也就是說遍歷不是從hash計算的下標到len-1就結束了,而是會遍歷全部數組,那麼再加上每次set
方法最後都會檢測槽位佔用情況來判斷是否進行rehash()
,就保證了數組是不會因爲滿而放不下數據的情況,另外prevIndex(int i, int len)
方法也是保證遍歷數組是頭尾相連的,提前知道這點有助於閱讀源碼時的理解速度
/**
* 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);
}
HASH算法
作爲Map,需要對Key進行Hash算法計算得到對應下標,而ThreadLocalMap的算法如下:
int i = key.threadLocalHashCode & (len-1);
前面重點提到過數組的長度是二的冪次,所以(len-1)表示二進制裏所有位都是1,比如2的3次方減1的二進制表示:1111,這裏進行與操作作用就是從threadLocalHashCode數值裏取len對應的位數部分。
/**
* 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 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;
/**
* The next hash code to be given out. Updated atomically. Starts at
* zero.
*/
private static AtomicInteger nextHashCode = new AtomicInteger();
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
從上面的代碼可以看到threadLocalHashCode
是nextHashCode變量加一個魔法數(HASH_INCREMENT),而nextHashCode是類靜態公共變量,其AtomicInteger類型確保nextHashCode變量是線程安全的,在new出ThreadLocal的時候,會執行到nextHashCode()
計算threadLocalHashCode
,這樣每個ThreadLocal拿到的threadLocalHashCode都是不同,並且是按一個固定差值規則產生的。
0x61c88647
這個神奇的數字一定有玄機吧。這就是傳說中的斐波那契散列。
0x61c88647
轉成十進制爲:-2654435769
黃金分割數計算公式:(Math.sqrt(5) - 1)/2
以下計算可得-2654435769
double t = (Math.sqrt(5) - 1)/2;
BigDecimal.valueOf(Math.pow(2,32)* t).intValue()
每次哈希的計算就是每次累加黃金比例參數乘以2的32冪次,然後再和2的冪次取模,如此得到分佈均勻的哈希值。
resize方法
擴容操作是隻有在set
方法觸發,對於數組爲基礎的數據結構大多會需要考慮擴容的能力。
/**
* Double the capacity of the table.
* 擴大兩倍容量
*/
private void resize() {
Entry[] oldTab = table;
int oldLen = oldTab.length;
int newLen = oldLen * 2;
// 新建數組
Entry[] newTab = new Entry[newLen];
int count = 0;
// 遍歷老數組上全部元素,重新計算hash值然後分配到新數組上
for (int j = 0; j < oldLen; ++j) {
Entry e = oldTab[j];
if (e != null) {
ThreadLocal<?> k = e.get();
// 過期數據
if (k == null) {
e.value = null; // Help the GC
} else {
// 重新根據新的數據長度計算hash值
int h = k.threadLocalHashCode & (newLen - 1);
while (newTab[h] != null)
h = nextIndex(h, newLen);
newTab[h] = e;
count++;
}
}
}
// 設置擴容閥值 - 固定是數組長度的三分之二
setThreshold(newLen);
// 重置已經使用的數組數量
size = count;
// 重置數組
table = newTab;
}
/**
* Set the resize threshold to maintain at worst a 2/3 load factor.
*/
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
remove方法
/**
* Remove the entry for key.
*/
private void remove(ThreadLocal<?> key) {
Entry[] tab = table;
int len = tab.length;
// 一樣的hash算法
int i = key.threadLocalHashCode & (len-1);
// 因爲採用開放地址法解決hash衝突,
// 所以找到下標後開始往後一個個比對,直到找打要刪除的key對應的Entry
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
// 比對key
if (e.get() == key) {
// Entry是WeakReference,清除弱引用
e.clear();
// 執行一次探測式清理
expungeStaleEntry(i);
return;
}
}
}
replaceStaleEntry方法
在set
方法中的場景三中,是調用replaceStaleEntry方法然後結束,從場景角度看,這個方法只需要幫忙做一個事,就是幫我們替換下新的值到已經過期的槽中就行了。然而事情不簡單,我們通過下圖瞭解怎麼不簡單。
我們假設數組中4下標是hash衝突的值,此時已經有四個元素hash衝突後依次放在4,5,6,7的槽位裏,然後5號槽位過期了。
此時set進來一個value,計算出來下標是4,發現4號槽位不爲null,並且key也不爲null,就繼續往後找,那麼就會發現5號槽位是已過期數據,如果直接替換就會發生圖中的情況,會有兩個相同key的Entry存入數組中,明顯這已經不符合Map的特性。
有的同學應該會馬上想到可以依次比對4個槽位,然後找出7號槽位進行替換就可以了,的確,這個思路沒錯。再思考下兩個點:
- 現在已經明顯發現有一個過期數據在哪個槽位
- 如果需要遍歷後續的槽位,那麼也可以判斷出哪些是過期數據
帶着這個思路,接下來我們查看replaceStaleEntry
源碼,看下它是怎麼實現的。
/**
* Replace a stale entry encountered during a set operation
* with an entry for the specified key. The value passed in
* the value parameter is stored in the entry, whether or not
* an entry already exists for the specified key.
*
* As a side effect, this method expunges all stale entries in the
* "run" containing the stale entry. (A run is a sequence of entries
* between two null slots.)
*
* @param key the key
* @param value the value to be associated with key
* @param staleSlot index of the first stale entry encountered while
* searching for key.
*/
private void replaceStaleEntry(ThreadLocal<?> key, Object value,
int staleSlot) {
Entry[] tab = table;
int len = tab.length;
Entry e;
// Back up to check for prior stale entry in current run.
// We clean out whole runs at a time to avoid continual
// incremental rehashing due to garbage collector freeing
// up refs in bunches (i.e., whenever the collector runs).
int slotToExpunge = staleSlot;
// 從staleSlot位置開始往前面循環,直到最近的空槽出現才停止,
// 注意這個prevIndex也是環形頭尾相連的
for (int i = prevIndex(staleSlot, len);
(e = tab[i]) != null;
i = prevIndex(i, len))
// 在循環過程中如果出現過期數據,則把下標賦值給slotToExpunge
// 注意,如果沒有出現過期數據,slotToExpunge=staleSlot
if (e.get() == null)
slotToExpunge = i;
// Find either the key or trailing null slot of run, whichever
// occurs first
// 從staleSlot位置開始往後面循環,直到最近的空槽出現才停止
for (int i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
// If we find key, then we need to swap it
// with the stale entry to maintain hash table order.
// The newly stale slot, or any other stale slot
// encountered above it, can then be sent to expungeStaleEntry
// to remove or rehash all of the other entries in run.
// 這裏註釋解釋得比較清楚,就是如果我們找到了相同key的槽,那麼就只需要替換舊的value就行了
// 另外,就是清理過期數據
if (k == key) {
// 賦新值
e.value = value;
// 交換邏輯,就是把staleSlot槽位entry和i槽位進行交換
// 交換後那個過期數據就到了i下標的槽位上
tab[i] = tab[staleSlot];
tab[staleSlot] = e;
// Start expunge at preceding stale entry if it exists
// 如果slotToExpunge依舊等於staleSlot表示在向前循環查找過期數據時沒有出現過期數據
// 並且在向後循環遍歷中也沒有出現過期數據,因爲一旦出現過就會在下面的代碼中把i賦值給slotToExpunge
// 所以此時需要清理的起始位置就是i
if (slotToExpunge == staleSlot)
slotToExpunge = i;
// 觸發清理操作
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
return;
}
// If we didn't find stale entry on backward scan, the
// first stale entry seen while scanning for key is the
// first still present in the run.
// k等於null,代表着出現過期數據了,
// 那麼如果此時slotToExpunge還是等於staleSlot說明在向前的循環中沒有出現過期數據
// 清理過期數據就可以從i開始,因爲即使在接下去的循環中出現過期數據,這個判斷也不成立了因爲slotToExpunge已經不等於staleSlot了
if (k == null && slotToExpunge == staleSlot)
slotToExpunge = i;
}
// If key not found, put new entry in stale slot
// 向後遍歷後沒有發現重複key的槽位,那麼就可以在這個槽位上替換這個過期數據了
tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value);
// If there are any other stale entries in run, expunge them
// 這裏意思是如果slotToExpunge等於staleSlot,
// 就表示除了staleSlot位置是過期數據,向前遍歷和向後遍歷都沒有找到一個過期數據
// staleSlot位置已經替換好值了,就不需要進行清理數據
// 但是隻要兩者是不相同的說明還有其他的過期數據,就需要進行清理操作
if (slotToExpunge != staleSlot)
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}
看完replaceStaleEntry
源碼,印證了前面的解決思路,代碼中不僅只是在往後遍歷時查看了數據是否過期,還向前遍歷查看了一些。
如何找到清理開始位置
對於如何找到清理開始位置,這個方法中寫了很多代碼,目的是以staleSlot爲起點向前找到第一個空槽位,然後以staleSlot爲起點向後找到最近的空槽位,如果在設置好新值後這兩個空槽位之間還存在過期數據,就觸發清理操作。
先遍歷前面數據:
-
前面有過期數據:從staleSlot向前遍歷時發現了過期數據,slotToExpunge則會設置成過期數據的下標,有多個過期數據的情況則slotToExpunge會設置成最遠的下標
-
前面沒有過期數據:slotToExpunge等於staleSlot
然後遍歷後面數據:
- 當後面有過期數據情況,如果前面沒有過期數據,則把slotToExpunge設置成staleSlot後面的過期數據槽位下標,精確了開始位置
- 當發現有key值相同情況,此時是把staleSlot上過期的數據和相同key槽位進行交換,所以此時已經明確相同key槽位是過期數據,那麼從這裏觸發往後面的清理,然後中斷往後的遍歷結束方法
最後兜底:
- 如果往後的循環沒有中斷就意味着沒有找到相同key的槽位,所以staleSlot下標的槽位就是要放數據的地方,新數據放入後,這個過期的槽位就不存在了,從前面兩個遍歷來看,只要發現新的過期數據slotToExpunge必然變化,並且保證slotToExpunge是最前面的過期槽位,所以只需要判斷slotToExpunge是否還等於staleSlot,如果不等於說明遍歷環節時除了staleSlot位置,還有其他槽位上有過期數據,那麼就觸發從slotToExpunge位置往後進行清理
清理操作
代碼如下:
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
清理工作就是進行expungeStaleEntry方法從slotToExpunge位置往後到最近的空槽位探測式清理,然後使用cleanSomeSlots
方法進行啓發式清理。
解釋一下這兩種清理方式
-
探測式清理(expungeStaleEntry)
這個就是正常遍歷下去一個個查驗是否過期,然後清除
-
啓發式清理(cleanSomeSlots)
這個是在需要遍歷的數據比較多的時候,要將全部的過期數據全部清理是比較耗時的,在正常的場景下,不太可能定時進行清理,我們也發現實際清理的觸發是在
set
方法或remove
方法,所以就進行挑選幾個形式的方式進行查驗和清除操作
expungeStaleEntry方法
先看探測式清理下expungeStaleEntry
方法,在replaceStaleEntry
和remove
方法中都有用到,在staleSlot下標到下一個空槽中間,擦除全部過期數據
/**
* Expunge a stale entry by rehashing any possibly colliding entries
* lying between staleSlot and the next null slot. This also expunges
* any other stale entries encountered before the trailing null. See
* Knuth, Section 6.4
*
* @param staleSlot index of slot known to have null key
* @return the index of the next null slot after staleSlot
* (all between staleSlot and this slot will have been checked
* for expunging).
*/
// staleSlot傳入參數就是已經知道這個位置是過期數據,不僅把這個位置清除一下
// 還會向後繼續遍歷到最近的空槽,遍歷過程中有過期數據也會清理
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// expunge entry at staleSlot
// staleSlot下標位置擦除操作
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
// Rehash until we encounter null
Entry e;
int i;
// 從staleSlot下標開始往後查
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
// key是null的情況,key已被回收,是過期數據
if (k == null) {
// 執行這個方法的任務:擦除操作
e.value = null;
tab[i] = null;
// 同步數組已用量
size--;
} else {// key不爲空的情況
// 進行一次hash計算,得出這個數據應該在的下標位置h
int h = k.threadLocalHashCode & (len - 1);
// 應該在的下標位置和當前下標進行比較 如果不相等,就表示這個數據是在放入的時候出現hash衝突,往後移動了位置的
// 那麼在擦除的操作裏可能它的前面已經有過期數據被擦除了,就需要把它往前移上去
if (h != i) {
// 開始移動
tab[i] = null;
// Unlike Knuth 6.4 Algorithm R, we must scan until
// null because multiple entries could have been stale.
// 確定移到的位置:從h下標開始往後找到最近的空槽
while (tab[h] != null)
h = nextIndex(h, len);
// 移動位置
tab[h] = e;
}
}
}
return i;
}
看來這個方法需要做的是就是遍歷數據然後檢查是否過期,如果是就清理過期數據,但是又是因爲開放地址法,所有hash衝突的數據都是確保挨着的,當你在清理數據的時候是不是需要考慮到一個場景:
假設4,5,6槽位的數據在放入的時候,key的hash值是相同的,都是4,所以按照順序挨着放在一起,那麼此時5號槽位上的key過期了,那麼觸發expungeStaleEntry
方法的時候會清理這個槽位,如果不做其他操作,4,6槽位的數據中間就有一個放着null的槽位。在get
方法時如果要找到6槽位的的key必然是先得到hash值是4,然後比對key1,發現不相同,就往後移動繼續比對key,但是因爲中間的空槽,6槽位就無法到達。所以就需要再做一件事就是把後面的6槽位往前移動,往前移動的依據就是重新計算hash值,然後再按照set
方法一樣再放一遍。
cleanSomeSlots方法
和前面探測式清理對應的啓發式清理是在方法註釋中就解釋了: Heuristically scan
。前面的已經解釋過這個機制的作用。
依據源碼,我們詳細瞭解一下這個機制的實現細節。因爲這個方法比較特殊,作者在註釋中解釋得比較清楚,所以先機器翻譯下這個註釋來理解下會比較好。
啓發式掃描一些單元格以查找過時的條目。 這在添加新元素或刪除另一個陳舊元素時調用。 它執行對數掃描,作爲不掃描(快速但保留垃圾)和掃描次數與元素數量成正比之間的平衡,這將找到所有垃圾但會導致某些插入花費 O(n) 時間
參數
n
控制掃描元素個數,在沒有過期數據被掃描到的情況下,會掃描log2(n)
個元素,反之,則需要增加掃描log2(table.length)-1
個元素。這個方法在set
調用時,n
爲元素數size
,當replaceStaleEntry
調用時傳的是數組長度。
方法中的while
循環條件(n >>>= 1) != 0
的意思是n向右移一位如果等於0則退出循環,這個其實就是n的2的冪次數累減然後判斷是否不等於0,換算成數學公式就是:log2(n)-1 != 0
。
/**
* Heuristically scan some cells looking for stale entries.
* This is invoked when either a new element is added, or
* another stale one has been expunged. It performs a
* logarithmic number of scans, as a balance between no
* scanning (fast but retains garbage) and a number of scans
* proportional to number of elements, that would find all
* garbage but would cause some insertions to take O(n) time.
*
* @param i a position known NOT to hold a stale entry. The
* scan starts at the element after i.
*
* @param n scan control: {@code log2(n)} cells are scanned,
* unless a stale entry is found, in which case
* {@code log2(table.length)-1} additional cells are scanned.
* When called from insertions, this parameter is the number
* of elements, but when from replaceStaleEntry, it is the
* table length. (Note: all this could be changed to be either
* more or less aggressive by weighting n instead of just
* using straight log n. But this version is simple, fast, and
* seems to work well.)
*
* @return true if any stale entries have been removed.
*/
private boolean cleanSomeSlots(int i, int n) {
boolean removed = false;
Entry[] tab = table;
int len = tab.length;
do {
// 遍歷
i = nextIndex(i, len);
Entry e = tab[i];
// 判斷是否過期
if (e != null && e.get() == null) {
// 出現過期數據 n就設置成數組長度,相當於如果有出現過期數據,就增加掃描次數,這裏是增大n來實現
n = len;
removed = true;
// 出現過期數據就用expungeStaleEntry方法清理,所以這裏是會從i下標到下個空槽內都會掃描一遍
i = expungeStaleEntry(i);
}
} while ( (n >>>= 1) != 0);
return removed;
}
這種沒有進行全量掃描的清理過期數據的方式在很多類似場景中都有相似的實現,比如Redis
的Key
主動刪除方式:
Specifically this is what Redis does 10 times per second:
- Test 20 random keys from the set of keys with an associated expire.
- Delete all the keys found expired.
- If more than 25% of keys were expired, start again from step 1.
注意如果在檢查的樣本中有百分之二十五是過期的,就再繼續進行清理,和上面的擴大n是一個思想。
getEntry方法
這個操作是Map結構提供出來get操作,比較簡單。也注意到hash值相同的Entry都是挨着的,如此纔可以保證get
到正確的value
。
/**
* Get the entry associated with key. This method
* itself handles only the fast path: a direct hit of existing
* key. It otherwise relays to getEntryAfterMiss. This is
* designed to maximize performance for direct hits, in part
* by making this method readily inlinable.
*
* @param key the thread local object
* @return the entry associated with key, or null if no such
*/
private Entry getEntry(ThreadLocal<?> key) {
// 熟悉的計算hash值的方式
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
// 找到key情況 直接返回值
if (e != null && e.get() == key)
return e;
else // 沒找到就需要用到開放地址法找key的流程了
return getEntryAfterMiss(key, i, e);
}
/**
* Version of getEntry method for use when key is not found in
* its direct hash slot.
*
* @param key the thread local object
* @param i the table index for key's hash code
* @param e the entry at table[i]
* @return the entry associated with key, or null if no such
*/
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
// 在遇到空槽之前的數據都遍歷
while (e != null) {
ThreadLocal<?> k = e.get();
if (k == key)
return e;
// 遇到過期數據,則進行清理操作
if (k == null)
expungeStaleEntry(i);
else
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
構造方法
ThreadLocalMap
的構造方法有兩個,第二個方法是專門用於可集成的ThreadLocal,只被ThreadLocal的createInheritedMap
方法調用,特殊的就是把傳入的ThreadLocalMap
遍歷複製到新的ThreadLocalMap
。
/**
* Construct a new map initially containing (firstKey, firstValue).
* ThreadLocalMaps are constructed lazily, so we only create
* one when we have at least one entry to put in it.
*/
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
/**
* Construct a new map including all Inheritable ThreadLocals
* from given parent map. Called only by createInheritedMap.
*
* @param parentMap the map associated with parent thread.
*/
private ThreadLocalMap(ThreadLocalMap parentMap) {
Entry[] parentTable = parentMap.table;
int len = parentTable.length;
setThreshold(len);
table = new Entry[len];
for (int j = 0; j < len; j++) {
Entry e = parentTable[j];
if (e != null) {
@SuppressWarnings("unchecked")
ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
if (key != null) {
// childValue可擴展
// 默認InheritableThreadLocal是返回傳入的值,
// 所以特別注意傳入的parentMap的value和新創建的ThreadLocalMap的value是一個對象
Object value = key.childValue(e.value);
Entry c = new Entry(key, value);
int h = key.threadLocalHashCode & (len - 1);
while (table[h] != null)
h = nextIndex(h, len);
table[h] = c;
size++;
}
}
}
}
ThreadLocal
構造方法
/**
* Creates a thread local variable.
* @see #withInitial(java.util.function.Supplier)
*/
public ThreadLocal() {
}
1.8版本引入了函數式接口方式:withInitial
方法
/**
* Creates a thread local variable. The initial value of the variable is
* determined by invoking the {@code get} method on the {@code Supplier}.
*
* @param <S> the type of the thread local's value
* @param supplier the supplier to be used to determine the initial value
* @return a new thread local variable
* @throws NullPointerException if the specified supplier is null
* @since 1.8
*/
public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {
return new SuppliedThreadLocal<>(supplier);
}
/**
* An extension of ThreadLocal that obtains its initial value from
* the specified {@code Supplier}.
*/
static final class SuppliedThreadLocal<T> extends ThreadLocal<T> {
private final Supplier<? extends T> supplier;
SuppliedThreadLocal(Supplier<? extends T> supplier) {
this.supplier = Objects.requireNonNull(supplier);
}
@Override
protected T initialValue() {
return supplier.get();
}
}
在1.8版本之前很多jdk的代碼都是如BigDecimal
的代碼那樣使用匿名類的方式這麼寫的:
private static final ThreadLocal<StringBuilderHelper>
threadLocalStringBuilderHelper = new ThreadLocal<StringBuilderHelper>() {
@Override
protected StringBuilderHelper initialValue() {
return new StringBuilderHelper();
}
};
通過覆蓋initialValue()
方法,就可以在線程執行get()
方法時獲得初始值。
而1.8後,推薦的寫法就變成這樣:
private static final ThreadLocal<StringBuilderHelper>
threadLocalStringBuilderHelper = ThreadLocal.withInitial(() -> new StringBuilderHelper());
通過Supplier
簡化代碼的方式值得借鑑。
get方法
/**
* Returns the value in the current thread's copy of this
* thread-local variable. If the variable has no value for the
* current thread, it is first initialized to the value returned
* by an invocation of the {@link #initialValue} method.
*
* @return the current thread's value of this thread-local
*/
public T get() {
Thread t = Thread.currentThread();
// 拿到線程實例內的ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null) {
// this就是key
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
// 找不到情況執行
return setInitialValue();
}
/**
* Get the map associated with a ThreadLocal. Overridden in
* InheritableThreadLocal.
*
* @param t the current thread
* @return the map
*/
// 默認返回的是Thread的threadLocals
// 這個方法在子類InheritableThreadLocal中覆蓋後返回的是Thread的threadLocals
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
set方法
/**
* Sets the current thread's copy of this thread-local variable
* to the specified value. Most subclasses will have no need to
* override this method, relying solely on the {@link #initialValue}
* method to set the values of thread-locals.
*
* @param value the value to be stored in the current thread's copy of
* this thread-local.
*/
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
// 調用ThreadLocalMap的set方法
map.set(this, value);
else
// 線程實例還未初始化ThreadLocalMap的情況下,需要進行一次初始化
createMap(t, value);
}
/**
* Create the map associated with a ThreadLocal. Overridden in
* InheritableThreadLocal.
*
* @param t the current thread
* @param firstValue value for the initial entry of the map
*/
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
createInheritedMap方法
/**
* Factory method to create map of inherited thread locals.
* Designed to be called only from Thread constructor.
*
* @param parentMap the map associated with parent thread
* @return a map containing the parent's inheritable bindings
*/
static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
return new ThreadLocalMap(parentMap);
}
這個方法只有被Thread的構造方法所調用,看下調用代碼片段:
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
boolean inheritThreadLocals) {
...
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
...
}
其實就是把父線程的inheritableThreadLocals
通過createInheritedMap
方傳遞給子線程的inheritableThreadLocals。
我們在文章的開始就清楚Thread持有一個ThreadLocalMap來存儲本地變量,其實Thread還持有一個ThreadLocalMap是:
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
/*
* InheritableThreadLocal values pertaining to this thread. This map is
* maintained by the InheritableThreadLocal class.
*/
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
註釋中解釋,inheritableThreadLocals是InheritableThreadLocal
來維護的,我們看下是怎麼實現的。
InheritableThreadLocal
是繼承ThreadLocal的一個類,它覆寫了三個方法,後面兩個方法直接把原先默認的Thread的threadLocals
替換成了Thread的inheritableThreadLocals
。這樣所有ThreadLocal的方法關聯的ThreadLocalMap都會指向到Thread的inheritableThreadLocals
。因爲線程創建時默認是會把父線程的inheritableThreadLocals
傳遞給子線程,所以只要使用InheritableThreadLocal,就相當於父線程也是使用inheritableThreadLocals
,子線程使用基於父線程的inheritableThreadLocals
。
public class InheritableThreadLocal<T> extends ThreadLocal<T> {
/**
* Computes the child's initial value for this inheritable thread-local
* variable as a function of the parent's value at the time the child
* thread is created. This method is called from within the parent
* thread before the child is started.
* <p>
* This method merely returns its input argument, and should be overridden
* if a different behavior is desired.
*
* @param parentValue the parent thread's value
* @return the child thread's initial value
*/
protected T childValue(T parentValue) {
return parentValue;
}
/**
* Get the map associated with a ThreadLocal.
*
* @param t the current thread
*/
ThreadLocalMap getMap(Thread t) {
return t.inheritableThreadLocals;
}
/**
* Create the map associated with a ThreadLocal.
*
* @param t the current thread
* @param firstValue value for the initial entry of the table.
*/
void createMap(Thread t, T firstValue) {
t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
}
}
而childValue
方法是爲了在使用InheritableThreadLocal
時的擴展,比如在createInheritedMap
方法創建ThreadLocalMap的時候,把父線程中的ThreadLocalMap的內容複製到子ThreadLocalMap中的時候會調用childValue
方法,可以進行擴展這個方法。例子代碼:
private InheritableThreadLocal<Map<String, String>> inheritableThreadLocal = new InheritableThreadLocal<Map<String, String>>() {
protected Map<String, String> childValue(Map<String, String> parentValue) {
return parentValue == null ? null : new HashMap(parentValue);
}
};
特別注意,在前面寫到的InheritableThreadLocal的childValue
方法是返回傳入的值,所以在ThreadLocalMap拿父線程的Map來構造的時候放入自己Map的Value其實是和父線程中Map所持有的是同一個對象,那麼在子線程使用的過程中修改了這個value,那麼父線程也會受到影響。
探索
關於static修飾
再很多規範甚至作者都推薦ThreadLocal
使用static
修飾,因爲這樣做的話可以避免重複創建ThreadLocal
,節省空間使用,並且是延長了ThreadLocal的生命週期,在平時研發代碼時,大多是線程共享的,比如一次請求的線程使用ThreadLocal,那麼ThreadLocal是請求級別,一般都需要調用remove
方法來清理線程內的本ThreadLocal對應的變量。
關鍵點是,前面已經介紹了ThreadLocalMap的Key是ThreadLocal的弱引用,如果適應static,那麼這個弱引用就相當於失效了,因爲static修飾的ThreadLocal始終有一個Class對象持有它,就不能觸發弱引用機制進行回收。其實這個也很矛盾,在源碼中做了一整套過期數據的清理機制,使用的時候一個static就沒什麼用了。
侷限
ThreadLocal不能解決多線程之間的本地變量傳遞,所以擴展出了InheritableThreadLocal,他能支持父線程給子線程來傳遞數據,但是任然沒有很好的解決在其他多線程之間的數據傳遞問題。
參考
https://juejin.cn/post/6947653879647617061#heading-8
https://segmentfault.com/a/1190000022663697
https://www.zhihu.com/question/35250439
https://jishuin.proginn.com/p/763bfbd652ed
https://juejin.cn/post/6844903938383151118
https://zhuanlan.zhihu.com/p/29520044
https://programmer.ink/think/deep-understanding-of-threadlocal-source-memory.html