java-ThreadLocal
ThreadLocal的實例代表了一個線程局部的變量涝焙,每條線程都只能看到自己的值锡凝,并不會意識到其它的線程中也存在該變量。它采用采用空間來換取時間的方式,解決多線程中相同變量的訪問沖突問題弹沽。
考慮一個問題:線程安全問題的核心在于多個線程會對同一個臨界區(qū)共享資源進行操作
synchronzed或者lock控制線程對臨界區(qū)資源的同步順序從而解決線程安全的問題。
ThreadLocal:表示線程的“本地變量”筋粗,即每個線程都擁有該變量副本策橘,每個線程都使用自己的“共享資源”,這就是一種“空間換時間”的方案娜亿。
ThreadLocal的內(nèi)部結(jié)構(gòu)
下圖是ThreadLocal的內(nèi)部結(jié)構(gòu)役纹,同時也簡單的給出來Thread對它的引用。
通過圖以及源碼 ThreadLocal內(nèi)部是基于一個ThreadLocalMap來實現(xiàn)暇唾,而ThreadLocalMap內(nèi)部又是一個Entry的數(shù)據(jù)結(jié)構(gòu)促脉。Entry的數(shù)據(jù)結(jié)構(gòu)是基于弱引用來使用辰斋。源碼結(jié)構(gòu)如下
public class ThreadLocal<T> {
static class ThreadLocalMap {
/**
* The table, resized as necessary.
* table.length MUST always be a power of two.
*/
private Entry[] table;
...
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
...
}
ThreadLocal的數(shù)據(jù)結(jié)構(gòu)分析
ThreadLocalMap詳解
ThreadLocal內(nèi)部定義了ThreadLocalMap靜態(tài)內(nèi)部類,數(shù)據(jù)真正存放在ThreadLocalMap當(dāng)中瘸味,所以threadLocal的get宫仗,set和remove方法實際上具體是通過threadLocalMap的getEntry,set和remove方法實現(xiàn)的。
Entry詳解
ThreadLocalMap是threadLocal一個靜態(tài)內(nèi)部類旁仿,和大多數(shù)容器一樣內(nèi)部維護了一個數(shù)組藕夫,同樣的threadLocalMap內(nèi)部維護了一個Entry類型的table數(shù)組。
Entry是一個以ThreadLocal為key,Object為value的鍵值對枯冈,另外需要注意的是這里的threadLocal是弱引用毅贮,因為Entry繼承了WeakReference,在Entry的構(gòu)造方法中尘奏,調(diào)用了super(k)方法就會將threadLocal實例包裝成一個WeakReferenece滩褥。
threadLocal內(nèi)存泄漏問題
每個線程實例中可以通過threadLocals獲取到threadLocalMap,而threadLocalMap實際上就是一個以threadLocal實例為key炫加,任意對象為value的Entry數(shù)組瑰煎。當(dāng)我們?yōu)閠hreadLocal變量賦值,實際上就是以當(dāng)前threadLocal實例為key俗孝,值為value的Entry往這個threadLocalMap中存放酒甸。需要注意的是Entry中的key是弱引用,當(dāng)threadLocal外部強引用被置為null(threadLocalInstance=null
),那么系統(tǒng) GC 的時候赋铝,根據(jù)可達性分析插勤,這個threadLocal實例就沒有任何一條鏈路能夠引用到它,這個ThreadLocal勢必會被回收革骨,這樣一來饮六,ThreadLocalMap中就會出現(xiàn)key為null的Entry,就沒有辦法訪問這些key為null的Entry的value苛蒲,如果當(dāng)前線程再遲遲不結(jié)束的話卤橄,這些key為null的Entry的value就會一直存在一條強引用鏈:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永遠(yuǎn)無法回收,造成內(nèi)存泄漏臂外。當(dāng)然窟扑,如果當(dāng)前thread運行結(jié)束,threadLocal漏健,threadLocalMap,Entry沒有引用鏈可達嚎货,在垃圾回收的時候都會被系統(tǒng)進行回收。在實際開發(fā)中蔫浆,會使用線程池去維護線程的創(chuàng)建和復(fù)用殖属,比如固定大小的線程池,線程為了復(fù)用是不會主動結(jié)束的瓦盛,所以洗显,threadLocal的內(nèi)存泄漏問題
ThreadLocal的數(shù)據(jù)操作分析
ThreadLocal的數(shù)據(jù)結(jié)構(gòu)與內(nèi)部組件外潜。通過提供的核心api了解這些內(nèi)部組件是如何進行數(shù)據(jù)存儲。
T get()
get方法是獲取當(dāng)前線程中threadLocal變量的值,具體步驟如下
- 獲取當(dāng)前線程的實例對象
- 獲取當(dāng)前線程的threadLocalMap
- if -> map != null 獲取map中當(dāng)前threadLocal實例為key的值的entry
- 當(dāng)前entitiy不為null的話挠唆,就返回相應(yīng)的值value
- 若map為null或者entry為null的話通過setInitialValue方法初始化处窥,并返回該方法返回的value
public T get() {
Thread t = Thread.currentThread();
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();
}
threadLocal 實線程隔離,其實就是Thread內(nèi)部自己維護ThreadLocalMap的一個成員變量玄组,getMap(t);方法也是證明的這個理論滔驾。
public class Thread implements Runnable {
...
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
...
}
setInitialValue 看下setInitialValue主要做了些什么事情?
邏輯和set方法幾乎一致俄讹,另外值得關(guān)注的是initialValue方法:protected修飾的也就是說繼承ThreadLocal的子類可重寫該方法哆致,實現(xiàn)賦值為其他的初始值
private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
protected T initialValue() {
return null;
}
get()總結(jié):
- 通過當(dāng)前線程thread實例獲取到它所維護的threadLocalMap,
- 然后以當(dāng)前threadLocal實例為key獲取該map中的鍵值對(Entry)患膛,若Entry不為null則返回Entry的value摊阀。
- 如果獲取threadLocalMap為null或者Entry為null的話,就以當(dāng)前threadLocal為Key剩瓶,value為null存入map后驹溃,并返回null城丧。
void set(T value)
set方法設(shè)置在當(dāng)前線程中threadLocal變量的值.方法的邏輯很簡單延曙,數(shù)據(jù)value是真正的存放在了ThreadLocalMap這個容器中了,并且是以當(dāng)前threadLocal實例為key
- 獲取當(dāng)前線程實例對象亡哄;
- 通過當(dāng)前線程實例獲取到ThreadLocalMap對象枝缔;
- 如果Map不為null,則以當(dāng)前threadLocl實例為key,值為value進行存入;
- 4.map為null,則新建ThreadLocalMap并存入value
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
分析
線程使用 ThreadLocalMap 來存儲每個線程副本變量蚊惯,它是 ThreadLocal 里的一個靜態(tài)內(nèi)部類愿卸。ThreadLocalMap 也是采用的散列表(Hash)思想來實現(xiàn)的,但是實現(xiàn)方式和 HashMap 不太一樣截型。
補充:散列表1
理想狀態(tài)下趴荸,散列表就是一個包含關(guān)鍵字的固定大小的數(shù)組,通過使用散列函數(shù)宦焦,將關(guān)鍵字映射到數(shù)組的不同位置发钝。
在理想狀態(tài)下,哈希函數(shù)可以將關(guān)鍵字均勻的分散到數(shù)組的不同位置波闹,不會出現(xiàn)兩個關(guān)鍵字散列值相同(假設(shè)關(guān)鍵字?jǐn)?shù)量小于數(shù)組的大性秃馈)的情況。但是在實際使用中精堕,經(jīng)常會出現(xiàn)多個關(guān)鍵字散列值相同的情況(被映射到數(shù)組的同一個位置)孵淘,我們將這種情況稱為散列沖突。為了解決散列沖突歹篓,主要采用下面兩種方式:
分離鏈表法(separate chaining)
開放定址法(open addressing)
補充:分離鏈表法
- 分散鏈表法使用鏈表解決沖突瘫证,將散列值相同的元素都保存到一個鏈表中揉阎。當(dāng)查詢的時候,首先找到元素所在的鏈表痛悯,然后遍歷鏈表查找對應(yīng)的元素余黎。
- index key = hash_key % table.size;
補充:開放定址法
- 開放定址法不會創(chuàng)建鏈表,當(dāng)關(guān)鍵字散列到的數(shù)組單元已經(jīng)被另外一個關(guān)鍵字占用的時候载萌,就會嘗試在數(shù)組中尋找其他的單元惧财,直到找到一個空的單元。探測數(shù)組空單元的方式有很多扭仁,這里介紹一種最簡單的 -- 線性探測法讥此。線性探測法就是從沖突的數(shù)組單元開始象踊,依次往后搜索空單元,如果到數(shù)組尾部,再從頭開始搜索(環(huán)形查找);
- 如下圖:
ThreadLocalMap 中使用開放地址法來處理散列沖突藤树,而 HashMap 中使用的分離鏈表法。之所以采用不同的方式主要是因為:在 ThreadLocalMap 中的散列值分散的十分均勻瘦赫,很少會出現(xiàn)沖突箕般。并且 ThreadLocalMap 經(jīng)常需要清除無用的對象,使用純數(shù)組更加方便顽分。
看一下set方法徐许。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;
//根據(jù)threadLocal的hashCode確定Entry應(yīng)該存放的位置
int i = key.threadLocalHashCode & (len-1);
//采用開放地址法,hash沖突的時候使用線性探測
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
//覆蓋舊Entry
if (k == key) {
e.value = value;
return;
}
//當(dāng)key為null時卒蘸,說明threadLocal強引用已經(jīng)被釋放掉雌隅,那么就無法
//再通過這個key獲取threadLocalMap中對應(yīng)的entry,這里就存在內(nèi)存泄漏的可能性
if (k == null) {
//用當(dāng)前插入的值替換掉這個key為null的“臟”entry
replaceStaleEntry(key, value, i);
return;
}
}
//新建entry并插入table中i處
tab[i] = new Entry(key, value);
int sz = ++size;
//插入后再次清除一些key為null的“臟”entry,如果大于閾值就需要擴容
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
threadLocal的hashcode?
從源碼中我們可以清楚的看到threadLocal實例的hashCode是通過nextHashCode()方法實現(xiàn)的缸沃,該方法實際上總是用一個AtomicInteger加上0x61c88647來實現(xiàn)的恰起。0x61c88647這個數(shù)是有特殊意義的,它能夠保證hash表的每個散列桶能夠均勻的分布趾牧,這是Fibonacci Hashing
检盼,也正是能夠均勻分布,所以threadLocal選擇使用開放地址法來解決hash沖突的問題翘单。
怎樣確定新值插入到哈希表中的位置吨枉?
key.threadLocalHashCode & (len-1)
,同hashMap和ConcurrentHashMap等容器的方式一樣县恕,利用當(dāng)前key(即threadLocal實例)的hashcode與哈希表大小相與东羹,因為哈希表大小總是為2的冪次方,所以相與等同于一個取模的過程忠烛,這樣就可以通過Key分配到具體的哈希桶中去属提。而至于為什么取模要通過位與運算的原因就是位運算的執(zhí)行效率遠(yuǎn)遠(yuǎn)高于了取模運算。
怎樣解決hash沖突?
源碼中通過nextIndex(i, len)
方法解決hash沖突的問題冤议,該方法為((i + 1 < len) ? i + 1 : 0);
斟薇,也就是不斷往后線性探測,當(dāng)?shù)焦1砟┪驳臅r候再從0開始恕酸,成環(huán)形堪滨。
怎樣解決“臟”Entry?
在set方法的for循環(huán)中尋找和當(dāng)前Key相同的可覆蓋entry的過程中通過replaceStaleEntry方法解決臟entry的問題蕊温。如果當(dāng)前table[i]為null的話袱箱,直接插入新entry后也會通過cleanSomeSlots來解決臟entry的問題。
如何進行擴容义矛?
private int threshold; // Default to 0
/**
* The initial capacity -- MUST be a power of two.
*/
private static final int INITIAL_CAPACITY = 16;
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);
}
/**
* Set the resize threshold to maintain at worst a 2/3 load factor.
*/
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
第一次為threadLocal進行賦值的時候會創(chuàng)建初始大小為16的threadLocalMap,并且通過setThreshold方法設(shè)置threshold发笔,其值為當(dāng)前哈希數(shù)組長度乘以(2/3),也就是說加載因子為2/3(加載因子是衡量哈希表密集程度的一個參數(shù)凉翻,如果加載因子越大的話了讨,說明哈希表被裝載的越多,出現(xiàn)hash沖突的可能性越大制轰,反之前计,則被裝載的越少,出現(xiàn)hash沖突的可能性越小垃杖。同時如果過小男杈,很顯然內(nèi)存使用率不高,該值取值應(yīng)該考慮到內(nèi)存使用率和hash沖突概率的一個平衡缩滨,如hashMap,concurrentHashMap的加載因子都為0.75)势就。這里threadLocalMap初始大小為16泉瞻,加載因子為2/3脉漏,所以哈希表可用大小為:16*2/3=10,即哈希表可用容量為10袖牙。
從set方法中可以看出當(dāng)hash表的size大于threshold的時候侧巨,會通過resize方法進行擴容
private void resize() {
Entry[] oldTab = table;
int oldLen = oldTab.length;
//新數(shù)組為原數(shù)組的2倍
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 {
//重新確定entry在新數(shù)組的位置,然后進行插入
int h = k.threadLocalHashCode & (newLen - 1);
while (newTab[h] != null)
h = nextIndex(h, newLen);
newTab[h] = e;
count++;
}
}
}
//設(shè)置新哈希表的threshHold和size屬性
setThreshold(newLen);
size = count;
table = newTab;
}
新建一個大小為原來數(shù)組長度的兩倍的數(shù)組鞭达,然后遍歷舊數(shù)組中的entry并將其插入到新的hash數(shù)組中司忱,主要注意的是,在擴容的過程中針對臟entry的話會令value為null畴蹭,以便能夠被垃圾回收器能夠回收坦仍,解決隱藏的內(nèi)存泄漏的問題。
set()總結(jié):
- 通過當(dāng)前線程對象thread獲取該thread所維護的threadLocalMap;
- 若threadLocalMap不為null,則以threadLocal實例為key,值為value的鍵值對存入threadLocalMap;
- 若threadLocalMap為null的話叨襟,就新建threadLocalMap然后在以threadLocal為鍵繁扎,值為value的鍵值對存入即可。
remove
從map中刪除數(shù)據(jù),先獲取與當(dāng)前線程相關(guān)聯(lián)的threadLocalMap然后從map中刪除該threadLocal實例為key的鍵值對即可梳玫。
- 獲取當(dāng)前線程的threadLocalMap;
- 從map中刪除以當(dāng)前threadLocal實例為key的鍵值對.
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}