【JAVA併發編程系列】ThreadLocal
【1】ThreadLocal 類結構與關鍵屬性
//ThreadLocal 定義類時帶有泛型,
//說明 ThreadLocal 可以儲存任意格式的數據
//ThreadLocal 類是泛型的,可以放任意值
public class ThreadLocal<T>
//threadLocalHashCode 表示當前 ThreadLocal 的 hashCode
//用於計算當前 ThreadLocal 在 ThreadLocalMap 中的索引位置
private final int threadLocalHashCode = nextHashCode();
// 計算 ThreadLocal 的 hashCode 值(就是遞增)
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
//HASH_INCREMENT 轉化爲十進制是 1640531527,
//2654435769 轉換成 int 類型就是 -1640531527,
//2654435769 等於 (√5-1)/2 乘以 2 的 32 次方
//(√5-1)/2 就是黃金分割數,近似爲 0.618
//0x61c88647 理解爲一個黃金分割數乘以 2 的 32 次方,
//它可以保證 nextHashCode 生成的哈希值,均勻的分佈在 2 的冪次方上,
//且小於 2 的 32 次方
private static final int HASH_INCREMENT = 0x61c88647;
// nextHashCode 主要作用是當前 ThreadLocal 賦唯一值,計算當前 ThreadLocal 在 ThreadLocalMap 中的索引位置
// 被 static 修飾非常關鍵,因爲一個線程在處理業務的過程中,ThreadLocalMap 是會被 set 多個 ThreadLocal 的,
// 多個 ThreadLocal 就依靠 threadLocalHashCode 進行區分
// static + AtomicInteger 保證了在一臺機器上每個 ThreadLocal 的 threadLocalHashCode 是唯一的
private static AtomicInteger nextHashCode =
new AtomicInteger();
【2】ThreadLocalMap
ThreadLocalMap 本身就是一個簡單的 Map 結構,key 是 ThreadLocal,value 是 ThreadLocal 保存的值,底層是數組的數據結構
爲什麼使用弱引用
假設 threadLocal 使用的是強引用,在業務代碼中執行 threadLocalInstance=null
操作,以實現清理掉 threadLocal 實例的目的,但是因爲 threadLocalMap 的 Entry 強引用 threadLocal,因此在 gc 的時候進行可達性分析,threadLocal 依然可達,對 threadLocal 並不會進行垃圾回收,這樣就無法真正達到業務邏輯的目的;
// 數組中的每個節點值,WeakReference 是弱引用,當沒有引用指向時,會直接被回收
static class Entry extends WeakReference<ThreadLocal<?>> {
// 當前 ThreadLocal 關聯的值
Object value;
// WeakReference 的引用 referent 就是 ThreadLocal
// Entry 的 Key 是 ThreadLocal
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
// 數組的初始化大小
private static final int INITIAL_CAPACITY = 16;
// 存儲 ThreadLocal 的數組
private Entry[] table;
// 擴容的閾值, 默認是數組大小的三分之二
private int threshold;
//ThreadLocalMap : 構造函數
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);
}
ThreadLocalMap 解決散列衝突的方法,開放定址法,原因,在 ThreadLocalMap 中的散列值分散的十分均勻,很少會出現衝突並且 ThreadLocalMap 經常需要清除無用的對象,使用純數組更加方便;
【3】ThreadLocal 是如何做到線程之間數據隔離的
ThreadLocalMap 是線程的屬性,所以每個線程的 ThreadLocals 都是隔離獨享的,父線程在創建子線程的情況下,會拷貝 inheritableThreadLocals 的值,但不會拷貝 threadLocals 的值;
【4】ThreadLocal 的 set 方法
// set 方法的主要作用是往當前 ThreadLocal 裏面設置值
// set 操作每個線程都是串行的,不會有線程安全的問題
public void set(T value) {
//獲取當前線程
Thread t = Thread.currentThread();
//獲取當前線程的 ThreadLocalMap 對象
ThreadLocalMap map = getMap(t);
// 當前 thradLocal 之前有設置值,直接設置,否則初始化
// map 存在則設置值
if (map != null)
map.set(this, value);
else
// 初始化ThreadLocalMap
createMap(t, value);
}
//返回對應線程的 ThreadLocalMap 對象
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
//初始化線程對象的 threadLocals 成員變量
//this : 當前 ThreadLocal 類實例
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
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);
}
ThreadLocalMap 的 set 方法
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
// 計算 key 在數組中的下標
//根據threadLocal的hashCode確定Entry應該存放的位置
int i = key.threadLocalHashCode & (len-1);
//採用開放地址法,hash衝突的時候使用線性探測
// 查看 i 索引位置有沒有值,有值的話,索引位置 + 1,直到找到沒有值的位置
// 這種解決 hash 衝突的策略,也導致了其在 get 時查找策略有所不同,體現在 getEntryAfterMiss 中
for (Entry e = tab[i];
e != null;
// nextIndex 就是讓在不超過數組長度的基礎上,把數組的索引位置 + 1
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
// 找到內存地址一樣的 ThreadLocal,直接替換
if (k == key) {
e.value = value;
return;
}
// 當前 key 是 null,說明 ThreadLocal 被清理了,直接替換掉
//當key爲null時,說明threadLocal強引用已經被釋放掉,那麼就無法
//再通過這個key獲取threadLocalMap中對應的entry,這裏就存在內存泄漏的可能性
if (k == null) {
//用當前插入的值替換掉這個key爲null的“髒”entry(stale entry)
replaceStaleEntry(key, value, i);
return;
}
}
// 當前 i 位置是無值的,可以被當前 thradLocal 使用
//新建entry並插入table中i處
tab[i] = new Entry(key, value);
int sz = ++size;
//插入後再次清除一些key爲null的“髒”entry(stale entry),如果大於閾值就需要擴容
// 當數組大小大於等於擴容閾值(數組大小的三分之二)時,進行擴容
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
ThreadLocalMap Entry 數組擴容
//擴容
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();
//遍歷過程中如果遇到髒entry的話直接令value爲null,
//使得value能夠被回收,解決隱藏的內存泄漏的問題
if (k == null) {
e.value = null; // Help the GC
} else {
// 計算 ThreadLocal 在新數組中的位置
int h = k.threadLocalHashCode & (newLen - 1);
// 如果索引 h 的位置值不爲空,往後+1,直到找到值爲空的索引位置
while (newTab[h] != null)
h = nextIndex(h, newLen);
// 給新數組賦值
newTab[h] = e;
count++;
}
}
}
// 給新數組初始化下次擴容閾值,爲數組長度的三分之二
setThreshold(newLen);
size = count;
table = newTab;
}
【5】ThreadLocal 的 get 方法
public T get() {
//獲取當前線程的實例對象
//因爲 threadLocal 屬於線程的屬性,所以需要先把當前線程拿出來
Thread t = Thread.currentThread();
//獲取當前線程的threadLocalMap
//從線程中拿到 ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null) {
//獲取map中當前threadLocal實例爲key的值的entry
//從 map 中拿到 entry,由於 ThreadLocalMap 在 set 時的 hash 衝突的策略不同,
//導致拿的時候邏輯也不太一樣
ThreadLocalMap.Entry e = map.getEntry(this);
// 如果不爲空,讀取當前 ThreadLocal 中保存的值
if (e != null) {
@SuppressWarnings("unchecked")
//當前entitiy不爲null的話,就返回相應的值value
T result = (T)e.value;
return result;
}
}
//若map爲null或者entry爲null的話通過該方法初始化,並返回該方法返回的value
//否則給當前線程的 ThreadLocal 初始化,並返回初始值 null
return setInitialValue();
}
private T setInitialValue() {
//initialValue() : 方法是protected修飾的,
//即繼承ThreadLocal的子類可重寫該方法,實現賦值爲其他的初始值
T value = initialValue();
//獲取當前線程的實例對象
Thread t = Thread.currentThread();
//獲取當前線程的threadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null)
//map 存在則設置值
map.set(this, value);
else
//初始化ThreadLocalMap
createMap(t, value);
//返回初始化值
return value;
}
ThreadLocalMap 的 getEntry 方法
// 得到當前 thradLocal 對應的值,值的類型是由 thradLocal 的泛型決定的
// 由於 thradLocalMap set 時解決 Hash 衝突的邏輯,導致 thradLocalMap get 時的邏輯也會相應不同
// 首先嚐試根據 hashcode 取模數組大小 = 索引位置i 尋找,找不到的話,自旋把 i+1,直到找到 thradLocal
private Entry getEntry(ThreadLocal<?> key) {
// 計算索引位置:ThreadLocal 的 hashCode 取模數組大小
int i = key.threadLocalHashCode & (table.length - 1);
// 根據索引i獲取entry
Entry e = table[i];
// e 不爲空,並且 e 的 ThreadLocal 的內存地址和 key 相同,直接返回,
// 否則就是沒有找到,繼續通過 getEntryAfterMiss 方法找
if (e != null && e.get() == key)
return e;
else
// 未查找到滿足條件的entry,額外在做的處理
return getEntryAfterMiss(key, i, e);
}
ThreadLocalMap 的 getEntryAfterMiss 方法
// 自旋 i+1,直到找到爲止
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
// 在大量使用不同 key 的 ThreadLocal 時,其實還蠻耗性能的
while (e != null) {
ThreadLocal<?> k = e.get();
// 內存地址一樣,表示找到了
if (k == key)
//找到和查詢的key相同的entry則返回
return e;
// 刪除沒用的 key
if (k == null)
//解決髒entry的問題
expungeStaleEntry(i);
else
// 繼續使索引位置 + 1
// 繼續向後環形查找
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
【6】ThreadLocal 的 remove 方法
public void remove() {
//獲取當前線程的threadLocalMap
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
//從map中刪除以當前threadLocal實例爲key的鍵值對
m.remove(this);
}
ThreadLocalMap 的 remove 方法
private void remove(ThreadLocal<?> key) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
//向後環形查找與 key 對應的 Entry
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
if (e.get() == key) {
//將entry的key置爲null
e.clear();
//將該entry的value也置爲null
expungeStaleEntry(i);
return;
}
}
}
【7】ThreadLocal 內存泄漏問題
【7.1】ThreadLocal 內存泄漏產生的原因
threadLocal threadLocalMap entry 之間的關係圖示
上圖中,實線代表強引用,虛線代表的是弱引用,如果threadLocal外部強引用被置爲null(threadLocalInstance=null)的話,threadLocal實例就沒有一條引用鏈路可達,很顯然在gc(垃圾回收)的時候勢必會被回收,因此entry就存在key爲null的情況,無法通過一個Key爲null去訪問到該entry的value。同時,就存在了這樣一條引用鏈:threadRef->currentThread->threadLocalMap->entry->valueRef->valueMemory,導致在垃圾回收的時候進行可達性分析的時候,value可達從而不會被回收掉,但是該value永遠不能被訪問到,這樣就存在了內存泄漏。
【7.2】ThreadLocal 內存泄漏的解決
ThreadLocalMap 的 cleanSomeSlots 方法
//參數 n 作用 :
//掃描控制 (scan control),從while中是通過n來進行條件判斷的,
//說明n就是用來控制掃描趟數(循環次數)的
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 = len;
removed = true;
//遇到“髒”entry(stale entry)
//調用 expungeStaleEntry 函數處理
i = expungeStaleEntry(i);
}
} while ( (n >>>= 1) != 0);
return removed;
}
ThreadLocalMap 的 expungeStaleEntry 方法
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
//清除當前髒entry
// expunge entry at staleSlot
// value置爲null後該value域變爲不可達,在下一次gc的時候就會被回收掉
// table[staleSlot]爲null後以便於存放新的entry
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
// Rehash until we encounter null
Entry e;
int i;
//往後環形繼續查找,直到遇到table[i]==null時結束
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
if (k == null) {
//遇到 k 爲 null 則清除當前髒 entry
e.value = null;
tab[i] = null;
size--;
} else {
//處理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;
}
}
}
return i;
}
ThreadLocalMap 的 replaceStaleEntry 方法
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).
//向前找到第一個髒entry
//現髒entry的相鄰位置也有很大概率出現髒entry,
//所以爲了一次處理到位,就需要向前環形搜索,找到前面的髒entry
int slotToExpunge = staleSlot;
for (int i = prevIndex(staleSlot, len);
(e = tab[i]) != null;
i = prevIndex(i, len))
if (e.get() == null)
slotToExpunge = i;
// Find either the key or trailing null slot of run, whichever
// occurs first
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.
if (k == key) {
//如果在向後環形查找過程中發現key相同的entry就覆蓋並且和髒entry進行交換
e.value = value;
tab[i] = tab[staleSlot];
tab[staleSlot] = e;
// Start expunge at preceding stale entry if it exists
//如果在查找過程中還未發現髒entry,那麼就以當前位置作爲cleanSomeSlots的起點
if (slotToExpunge == staleSlot)
slotToExpunge = i;
//搜索髒entry並進行清理
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.
//如果向前未搜索到髒entry,則在查找過程遇到髒entry的話,
//後面就以此時這個位置作爲起點執行cleanSomeSlots
if (k == null && slotToExpunge == staleSlot)
slotToExpunge = i;
}
// If key not found, put new entry in stale slot
//如果在查找過程中沒有找到可以覆蓋的entry則將新的entry插入在髒entry
tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value);
// If there are any other stale entries in run, expunge them
if (slotToExpunge != staleSlot)
//執行cleanSomeSlots
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}
replaceStaleEntry 處理過程圖示
1. 前向有髒 entry 後向環形查找找到可覆蓋的 entry
2. 前向有髒 entry 後向環形查找未找到可覆蓋的 entry
3. 前向沒有髒 entry 後向環形查找找到可覆蓋的 entry
4. 前向沒有髒 entry 後向環形查找未找到可覆蓋的 entry
ThreadLocalMap 的 getEntryAfterMiss 方法
// 自旋 i+1,直到找到爲止
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
// 在大量使用不同 key 的 ThreadLocal 時,其實還蠻耗性能的
while (e != null) {
ThreadLocal<?> k = e.get();
// 內存地址一樣,表示找到了
if (k == key)
//找到和查詢的key相同的entry則返回
return e;
// 刪除沒用的 key
if (k == null)
//解決髒entry的問題
expungeStaleEntry(i);
else
// 繼續使索引位置 + 1
// 繼續向後環形查找
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
ThreadLocalMap 的 remove 方法
private void remove(ThreadLocal<?> key) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
//向後環形查找與 key 對應的 Entry
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
if (e.get() == key) {
//將entry的key置爲null
e.clear();
//將該entry的value也置爲null
expungeStaleEntry(i);
return;
}
}
}
參考致謝
本博客爲博主學習筆記,同時參考了網上衆博主的博文以及相關專業書籍,在此表示感謝,本文若存在不足之處,請批評指正。
【1】不懂ThreadLocal,面試官很難相信你懂Java併發編程
【3】JAVA併發專題十七:深入理解ThreadLocal(一)