趣鏈
的面試中被問到ThreadLocal
的相關問題,被問的一臉懵*,因此有次總結.線程局部變量
是我一直對他的叫法,剛開始接觸是用來保存jdbc
的鏈接(這樣想一想我接觸的還挺早的)ThreadLocal
並非底層的集合類,而是一個工具類,全部的線程私有數據都被保存在各個Thread
對象中一個叫作threadLocals
的ThreadLocalMap
的成員變量裏,ThreadLocal
也只是操做這些變量的工具類.Thread
都會存有一個ThreadLocalMap
的對象供多個ThreadLocal
的類調用,因此你能夠發現多個ThreadLocal
操做的Map
會是同一個,而當ThreadLocal
做爲key
的發生哈希碰撞時,會從當前位置開始向後環型遍歷,找到一個空位置,這方法咱們能夠稱之爲線性探測法.ThreadLocalMap
出人意料的並無繼承任何一個類或接口,是徹底獨立的類。// 默認的初始容量 必定要是二的次冪
private static final int INITIAL_CAPACITY = 16;
// 元素數組/條目數組
private Entry[] table;
// 大小,用於記錄數組中實際存在的Entry數目
private int size = 0;
// 閾值
private int threshold; // Default to 0 構造方法
複製代碼
// 默認訪問權限的初始化方法
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
// 使用默認的`容量`初始化數組
table = new Entry[INITIAL_CAPACITY];
// 以`ThreadLocal`的`HashCode`計算下標
// 這裏和HashMap中的計算方式同樣,都用與運算
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
// 賦值 修改大小並計算閾值
table[i] = new Entry(firstKey, firstValue);
size = 1;
// `setThreshold`方法也特別簡單,就是2/3的容量。
setThreshold(INITIAL_CAPACITY);
}
複製代碼
以ThreadLocal
爲Key
獲取對應的Entry
。java
由於ThreadLocalMap
底層也是使用數組做爲數據結構,因此該方法也借鑑了HashMap
中求元素下標的方式.面試
在獲取的元素爲空的時候還會調用getEntryAfterMiss
作後續處理.數組
private Entry getEntry(ThreadLocal<?> key) {
// 和HashMap中同樣的下標計算方式
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
// 獲取到對應的Entry以後就分兩步
if (e != null && e.get() == key)
// 1. e不爲空且threadLocal相等
return e;
else
// 2. e爲空或者threadLocal不相等
return getEntryAfterMiss(key, i, e);
}
複製代碼
Hash
計算下標後,沒獲取到對應的Entry
對象的時候調用。key
表示的Entry
對象。private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
// 此時注意若是從上面狀況`2.`進來時,
// e爲空則直接返回null,不會進入while循環
// 只有e不爲空且e.get() != key時纔會進while循環
while (e != null) {
ThreadLocal<?> k = e.get();
// 找到相同的k,返回獲得的Entry,get操做結束
if (k == key)
return e;
// 若此時的k爲空,那麼e則被標記爲`Stale`須要被`expunge`
if (k == null)
expungeStaleEntry(i);
else // 下面兩個都是遍歷的相關操做
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
複製代碼
staleSlot
位置的Entry對象,而且會清理當前節點到下一個null
節點中間的過時Enyru
.內存泄漏
威脅的主力方法,在整個ThreadLocalMap
中會屢次調用./** * 清空舊的Entry對象 * @param staleSlot: 清理的起始位置 * @param return: 返回的是第一個爲空的Entry下標 */
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// 清空`staleSlot`位置的Entry
// value引用置爲空以後,對象被標記爲不可達,下次GC就會被回收.
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
Entry e;
int i;
// 經過nextIndex從`staleSlot`的下一個開始向後遍歷Entry數組,直到e不爲空
// e賦值爲當前的Entry對象
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
// 當k爲空的時候清空節點信息
if (k == null) {
e.value = null;
tab[i] = null;
size--;
} else { // 如下爲k存在的狀況
int h = k.threadLocalHashCode & (len - 1);
// 元素下標和key計算的不同,代表是出現`Hash碰撞`以後調整的位置
// 將當前的元素移動到下一個null位置
if (h != i) {
tab[i] = null;
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}
複製代碼
ThreadLocalMap
底層結構和HashMap
同樣也是數組,也是經過hash
肯定下標,也同樣會發生Hash碰撞
,咱們知道在HashMap
中爲了解決Hash碰撞
的問題選擇了拉鍊法,但對於ThreadLocalMap
並無那麼高的複雜度,因此此處選擇的是開放地址法
.Entry
再肯定數組位置以後直接就開始了遍歷,若是key
不匹配就日後遍歷找到key
匹配的元素覆蓋,或者key == null
的替換.private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
// 整個循環的功能就是找到相同的key覆蓋value
// 或者找到key爲null的節點覆蓋節點信息
// 只有在e==null的時候跳出循環執行下面的代碼
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
// 找到相等的k,則直接替換value,set操做結束
if (k == key) {
e.value = value;
return;
}
// k爲空表示該節點過時,直接替換該節點
if (k == null) { // 1.
replaceStaleEntry(key, value, i);
return;
}
}
// 走到這一步就是找到了e爲空的位置,否則在上面兩個判斷裏都return了
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
複製代碼
1.
處進入該方法,用於替換key
爲空的Entry
節點,順帶清除數組中的過時節點./** * 從`set.1.`處進入,key是插入元素ThreadLocal的hash,staleSlot爲key爲空的數組節點下標 */
private void replaceStaleEntry(ThreadLocal<?> key, Object value, int staleSlot) {
Entry[] tab = table;
int len = tab.length;
Entry e;
int slotToExpunge = staleSlot;
// 從傳入位置,即插入時發現k爲null的位置開始,向前遍歷,直到數組元素爲空
// 找到最前面一個key爲null的值.
// 這裏要吐槽一下源代碼...大括號都不加 習慣真差
for (int i = prevIndex(staleSlot, len);
(e = tab[i]) != null;
i = prevIndex(i, len)){
if (e.get() == null)
// 由於是環狀遍歷因此此時slotToExpunge是可能等於staleSlot的
slotToExpunge = i;
}
// 該段循環的功能就是向後遍歷找到`key`相等的節點並替換
// 並對以後的元素進行清理
for (int i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
if (k == key) {
// e就是tab[i],因此下三行代碼的功能就是替換Entry
// 新的Entry實際仍是在staleSlot下標的位置
e.value = value;
tab[i] = tab[staleSlot];
tab[staleSlot] = e;
// 由於接下來要進行清理操做,因此此處須要從新肯定清理的起點.
if (slotToExpunge == staleSlot)
slotToExpunge = i;
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
return;
}
// 其實我對這個`slotToExpunge == staleSlot`的判斷一直挺疑惑的,爲何須要這個判斷?
if (k == null && slotToExpunge == staleSlot)
slotToExpunge = i;
}
// e==null時跳到下面代碼運行
// 清空並從新賦值
tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value);
// set後的清理
if (slotToExpunge != staleSlot)
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}
複製代碼
Entry
i
向後開始遍歷log2(n)
次,若是之間發現過時Entry
會直接將n
擴充到len
能夠說全數組範圍的遍歷.發現過時Entry
就調用expungeStaleEntry
清除直到未發現Entry
爲止./** * @param i 清除的起始節點位置 * @param n 遍歷控制,每次掃描都是log2(n)次,通常取當前數組的`size`或`len` */
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) {
// 當發現有過時`Entry`時,n變爲len
// 即擴大範圍,全數組範圍在遍歷一次
n = len;
removed = true;
i = expungeStaleEntry(i);
}
// 無符號右移一位至關於n = n /2
// 因此在第一次會遍歷`log2(n)`次
} while ( (n >>>= 1) != 0);
// 遍歷過程當中沒出現過時`Entry`的狀況下會返回是否有清理的標記.
return removed;
}
複製代碼
Entry
,並作是否須要resize
的判斷private void rehash() {
// 清理過時Entry
expungeStaleEntries();
// 初始閾值threshold爲10
if (size >= threshold - threshold / 4)
resize();
}
複製代碼
private void resize() {
// 獲取舊數組並記錄就數組大小
Entry[] oldTab = table;
int oldLen = oldTab.length;
// 新數組大小爲舊數組的兩倍
int newLen = oldLen * 2;
Entry[] newTab = new Entry[newLen];
int count = 0;
// 遍歷整個舊數組,並遷移元素到新數組
for (int j = 0; j < oldLen; ++j) {
Entry e = oldTab[j];
// 判斷是否爲空,空的話就算了
if (e != null) {
ThreadLocal<?> k = e.get();
// k爲空即表示爲過時節點,立即清理了.
if (k == null) {
e.value = null;
} else {
// 從新計算數組下標,若是數組對應位置已存在元素
// 則環狀遍歷整個數組找個空位置賦值
int h = k.threadLocalHashCode & (newLen - 1);
while (newTab[h] != null)
h = nextIndex(h, newLen);
newTab[h] = e;
count++;
}
}
}
// 設置新屬性
setThreshold(newLen);
size = count;
table = newTab;
}
複製代碼
ThreadLocal
的內部方法由於邏輯都不復雜,不須要單獨出來看,就直接全放一塊了.ThreadLocal
的精華只要仍是在TheradLocalMap
和這種空間換時間的結構.
getMap
方法獲取當前線程綁定的threadLocals
ThreadLocal
對象爲參數獲取對應的Entry
對象.爲空跳到第四步Entry
對象中的value
,並返回setInitialValue
方法,// 直接獲取線程私有的數據
public T get() {
// 獲取當前線程
Thread t = Thread.currentThread();
// getMap其實很簡單就是獲取`t`中的`threadLocals`,代碼在`工具方法`中
ThreadLocalMap map = getMap(t);
if (map != null) { // 3.
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) { // 2.
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue(); // 1.
}
// 這個方法只有在上面`1.`處調用...不知道爲何map,thread不直接傳參
// 該方法的功能就是爲`Thread`設置`threadLocals`的初始值
private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
// map不爲null代表是從上面的`2.`處進入該方法
// 已經初始化`threadLocals`,但並未找到當前對應的`Entry`
// 因此此時直接添加`Entry`就行
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
// 初始值,`protected`方便子類繼承,並定義本身的初始值.
protected T initialValue() {
return null;
}
// 建立並賦值`threadLocals`的方法
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
複製代碼
ThreadLocalMap
對象.map
不爲空時,直接set就好map
爲空時須要先建立並賦值.public void set(T value) {
// 獲取當前線程
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t); // .1
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
複製代碼
t
中保留的ThreadLocalMap
類型的對象ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
複製代碼
首先對於對做爲key
的ThreadLocal
對象,由於是弱引用咱們徹底不用擔憂,強引用斷開以後天然會被GC
回收.安全
再來看value
,按照上面所說的做爲成員變量存儲在每一個Thread
實例的threadLocals
纔是存儲數據的對象,那麼它的生命週期是和Thread
相同的,即便將ThreadLocal
被GC
回收, 但對應的value
對象仍然存在thread -> threadLocals -> value引用 -> value對象
的引用關係,因此GC
會認爲它可達,並不會作回收處理,但在咱們現有的代碼中並無可以跳過key
去獲取value
的,也就是說實際上value
已經不可達了.這樣就形成了內存泄漏.數據結構
value
的引用關係,就是講value
引用置null
.ThreadLcoalMap
的方法多處調用了expungeStaleEntry
,cleanSomeSlots
檢查數組中的Entry
對象是否過時,也就是key
是否爲空.併發問題
在我理解中就是多線程狀況下對共享資源的合理使用,像是ReentrantLock
,Synchronized
都是幫咱們解決共享資源的使用問題.ThreadLocal
則幫咱們提供了另一種思路,就是在每個線程中保留副本,就是上文有提到的以空間換時間的形式保證資源的合理有序使用,因此我以爲也是解決併發問題的一種思路.