概述 1、在併發編程中,為了控制數據的正確性,我們往往需要使用鎖來來保證代碼塊的執行隔離性。但是在很多時候鎖的開銷太大了,而在某些情況下,我們的局部變數是線程私有的,每個線程都會有自己的獨自的變/量,這個時候我們可以不對這部分數據進行加鎖操作。於是ThredLocal應運而生。 2、ThredLoc ...
概述
1、在併發編程中,為了控制數據的正確性,我們往往需要使用鎖來來保證代碼塊的執行隔離性。但是在很多時候鎖的開銷太大了,而在某些情況下,我們的局部變數是線程私有的,每個線程都會有自己的獨自的變/量,這個時候我們可以不對這部分數據進行加鎖操作。於是ThredLocal
應運而生。
2、ThredLocal
顧名思義,是線程持有的本地變數,存放在ThredLocal
中的變數不會同步到其他線程以及主線程,所有線程對於其他的線程變數都是不可見的。那麼我們來看下它是如何實現的吧。
3、註意:光理論是不夠的。在此免費贈送5大JAVA架構項目實戰教程及大廠面試題庫,有興趣的可以進裙 783802103獲取,沒基礎勿進哦!
實現原理
ThredLocal
在內部實現了一個靜態類ThreadLocalMap
來對於變數進行存儲,並且在Thread
類的內部使用到了這兩個成員變數
ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
來調用ThreadLocalMap
存儲當前線程的內部變數。
ThreadLocalMap的實現
ThreadLocalMap
是鍵值對結構的map,但是他沒有直接使用HashMap
,而是自己實現了一個。
Entry
Entry
是ThreadLocalMap
中定義的map節點,他以ThreadLocal
弱引用為key,以Object為value的K-V
形式的節點。使用弱引用是為了可以及時釋放記憶體避免記憶體泄漏。
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
這裡和HashMap
不一樣的地方在於兩者解決hash衝突的方式的不同,HashMap
採用的是鏈地址法,遇到衝突的時候將衝突的數據放入同一鏈表之中,等到鏈表到了一定程度再將鏈表轉化為紅黑樹。而ThreadLocalMap
實現採用的是開放定址法,它內部沒有使用鏈表結構,因此Entry
內部沒有next或者是prev指針。ThreadLocalMap
的開放定址法是怎麼實現的,請看接下來的源碼。
成員變數
// map預設的初始化大小
private static final int INITIAL_CAPACITY = 16;
// 存儲map節點數據的數組
private Entry[] table;
// map大小
private int size = 0;
// 臨界值,達到這個值的時候需要擴容
private int threshold;
// 當臨界值達到2/3的時候擴容
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
這裡的數組大小始終是2的冪次,原因和HashMap
一樣,是為了在計算hash偏移的時候減少碰撞。
構造函數
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
// 初始化table
table = new Entry[INITIAL_CAPACITY];
// 計算第一個值的hash值
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
// 創建新的節點
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
set方法
private void set(ThreadLocal<?> key, Object value) {
// 獲取ThreadLocal的hash值偏移量
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
// 遍曆數組直到節點為空
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
// 如果節點key相等,即找到了我們想要的節點,
// 將值賦予節點
if (k == key) {
e.value = value;
return;
}
// 如果節點的key為空,說明弱引用已經把key回收了,那麼需要做一波清理
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
// 如果沒有找到對應的節點說明該key不存在,創建新節點
tab[i] = new Entry(key, value);
int sz = ++size;
// 進行清理,如果清理結果沒能清理掉任何的舊節點,
// 並且數組大小超出了臨界值,就進行rehash操作擴容
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
看到這段代碼,開放定址法的實現原理可以說是非常清楚了。首先計算節點的hash值,找到對應的位置,查看該位置是否為空,如果是空則插入,如果不為空,則順延至下個節點,直到找到空的位置插入。那麼我們的查詢邏輯也呼之欲出:計算節點的hash值,找到對應的位置,查看該節點是否是我們想要找的節點,如果不是,則繼續往下順序尋找。
get方法
private Entry getEntry(ThreadLocal<?> key) {
// 計算hash值
int i = key.threadLocalHashCode & (table.length - 1);
// 獲取該hash值對應的數組節點
Entry e = table[i];
if (e != null && e.get() == key)
// 如果節點不為空並且key一致,說明是我們找的節點,直接返回
return e;
else
// 否則繼續往後尋找
return getEntryAfterMiss(key, i, e);
}
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
// 如果節點不為空就一直找下去
while (e != null) {
ThreadLocal<?> k = e.get();
// key相同則說明找到,返回該節點
if (k == key)
return e;
// key為空進行一次清理
if (k == null)
expungeStaleEntry(i);
else
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
replaceStaleEntry
// 這個方法的作用是在set操作的時候進行清理
private void replaceStaleEntry(ThreadLocal<?> key, Object value,
int staleSlot) {
Entry[] tab = table;
int len = tab.length;
Entry e;
// slotToExpunge是之後開始清理的節點位置
int slotToExpunge = staleSlot;
// 往前尋找找到第一個為空的節點記錄下位置
for (int i = prevIndex(staleSlot, len);
(e = tab[i]) != null;
i = prevIndex(i, len))
if (e.get() == null)
slotToExpunge = i;
// 從staleSlot開始向後遍歷直到節點為空
for (int i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
if (k == key) {
// 如果節點的key一致,替換value的值
e.value = value;
// 將當前節點和staleSlot上的節點互換位置(將後方的值放到前方來,之前的值等待回收)
tab[i] = tab[staleSlot];
tab[staleSlot] = e;
// 如果slotToExpunge和staleSlot相等,說明前面沒有需要清理的節點
// 則從當前節點開始進行清理
if (slotToExpunge == staleSlot)
slotToExpunge = i;
// 進行節點清理
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
return;
}
// 如果key為空並且slotToExpunge和staleSlot相等
// 把slotToExpunge賦值為當前節點
if (k == null && slotToExpunge == staleSlot)
slotToExpunge = i;
}
// 如果沒法找到key相等的節點,
// 則清空當前節點的value並生成新的節點
tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value);
// 如果slotToExpunge和staleSlot不相等則需要進行清理(因為前方發現空的節點)
if (slotToExpunge != staleSlot)
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}
expungeStaleEntry
// 對節點進行清理
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// 釋放當節點
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
Entry e;
int i;
// 迴圈尋找到第一個空節點
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
// key為空進行節點釋放
if (k == null) {
e.value = null;
tab[i] = null;
size--;
} else {
// 如果key不為空,找到對應的節點應該在的位置
int h = k.threadLocalHashCode & (len - 1);
if (h != i) {
// 如果和當前節點位置不同,
// 則清理節點並且迴圈找到後面的非空節點移到前面來
tab[i] = null;
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}
cleanSomeSlots
// 該方法用於清理空節點
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];
// 如果有節點為空並且key為空
// 該節點需要被清除
if (e != null && e.get() == null) {
// 重置n的值並且標記removed為true
n = len;
removed = true;
// 清理該節點
i = expungeStaleEntry(i);
}
} while ( (n >>>= 1) != 0);
return removed;
}
rehash
// 當數組的元素到達臨界值,進行擴容
private void rehash() {
// 先對所有的節點進行清理
expungeStaleEntries();
// 然後判斷臨界值是否進行擴容
// 此處由於先做過一次清理,這裡的數字可能會和之前的臨界值判斷有縮小
// 所以此處臨界值判斷為threshold - threshold / 4
// 即1/2的size時進行擴容
if (size >= threshold - threshold / 4)
resize();
}
private void resize() {
// 獲取舊數組,開闢新數組
// 新數組大小為舊數組的2倍
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) {
// 如果節點不為空,判斷key是否為空
// 如果key為空,將節點置空幫助gc
// 如果key不為空將舊數組的節點放入新數組
// 放入方式和set實現一致,只是由於是剛創建的新數組
// 不會有需要清理的數據,所以不需要額外清理
ThreadLocal<?> k = e.get();
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;
}
expungeStaleEntries
// 清理所有節點
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);
}
}
關於Map的清理
ThreadLocalMap
實現採用的是開放定址法,它的實現本身應該是比較簡潔的,但是為了便於GC,內部節點採用了弱引用作為key,一旦數組中節點的強引用被設置為了null,節點的key就會被gc自動回收。這樣導致了ThreadLocalMap
的實現變得異常的複雜。為了防止記憶體泄漏,在get和set方法的時候不得不進行額外的清理。
Q 為什麼需要清理?
A 不清理的話key被回收,但是value依舊會存在,並且難以被回收導致記憶體泄漏。
Q 為什麼清理的時候會涉及到節點的移動?
A 因為在開放定址法中,可能會有相同hash值的節點連續排在一起,當其中的一個或多個節點被回收後會造成同hash值的節點中間存在null節點,而我們get節點的時候會在碰到空節點的時候停止尋找,所以如果不進行一定的清理移動會導致部分節點永遠不會被查詢到。
ThreadLocal的實現
hashcode的實現
講完了ThreadLocalMap
的實現原理,我們可以深深的體會到ThreadLocal
的hashcode
是多麼的重要,如果hash值不能夠以合理的方式生成,導致數據的分佈不均勻,ThreadLocalMap
的效率將會非常的低下。
hashcode的實現:
private final int threadLocalHashCode = nextHashCode();
private static AtomicInteger nextHashCode =
new AtomicInteger();
private static final int HASH_INCREMENT = 0x61c88647;
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
ThreadLocal
的hashcode
實現代碼很簡短:每一個新的ThreadLocal
的hash值都是在nextHashCode
的基礎上增加0x61c88647
。實現很簡單,但是很讓人迷惑。這個莫名其妙的魔數0x61c88647
是什麼?
0x61c88647
是有斐波那契構造而成的黃金比例數字,經過實驗測試,這個數字生成的hashcode可以很大程度的保證hash值能夠在數組中均勻的分佈。
get
public T get() {
// 獲取當前線程
Thread t = Thread.currentThread();
// 獲取當前線程的變數map
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();
}
set
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
// 如果map不為空,加入數據
if (map != null)
map.set(this, value);
else
// 否則新建map並放入第一個和數據
createMap(t, value);
}
總結
1、Thredlocal
這個類可能對於很多人來說是一個常常會用到的類,但是未必所有人都會去關註他的內部實現,但是他的源碼是比較值得去閱讀的,一來它的實現代碼相對其他的常用類很短,只有幾百行;二來它的實現很經典,經典的開放定址法,經典的弱引用方便GC,可以說是很好的學習材料。這裡我雖然對於整個Thredlocal
的源碼進行了完整的註釋解釋,但是它最值得細細品味的還是它的設計理念以及設計思路,這會對我們寫出優秀的代碼有著重要的作用。
2、註意:光理論是不夠的。在此免費贈送5大JAVA架構項目實戰教程及大廠面試題庫,有興趣的可以進裙 783802103獲取,沒基礎勿進哦!
本文的文字及圖片來源於網路加上自己的想法,僅供學習、交流使用,不具有任何商業用途,版權歸原作者所有,如有問題請及時聯繫我們以作處
最後