ThreadLocal是什么?
ThreadLocal是一個(gè)關(guān)于創(chuàng)建線程局部變量的類做裙。
通常情況下匙隔,我們創(chuàng)建的變量是可以被任何一個(gè)線程訪問并修改的。而使用ThreadLocal創(chuàng)建的變量只能被當(dāng)前線程訪問阁簸,其他線程則無法訪問和修改。
ThreadLocal使用示例
示例1:ThreadLocal聲明基本類型變量
??執(zhí)行程序哼丈,可以得到:
從運(yùn)行結(jié)果可以看出启妹,對(duì)于基本類型變量,ThreadLocal確實(shí)是可以達(dá)到線程隔離作用的醉旦。
示例2:ThreadLocal聲明自定義類型的對(duì)象
??執(zhí)行程序饶米,可以得到:
從運(yùn)行結(jié)果可以看出,對(duì)于自定義類型的對(duì)象车胡,ThreadLocal也是可以達(dá)到線程隔離作用的檬输。
示例3:ThreadLocal聲明的變量都指向同一個(gè)對(duì)象
對(duì)示例2的代碼稍作修改,使得ThreadLocal聲明的變量初始化時(shí)不再實(shí)例化一個(gè)新的對(duì)象匈棘,而是讓它指向同一個(gè)對(duì)象丧慈,運(yùn)行查看結(jié)果:
很顯然,在這里主卫,并沒有通過ThreadLocal達(dá)到線程隔離的機(jī)制逃默,可是ThreadLocal不是保證線程安全的么?這是什么鬼簇搅? 顯然完域,雖說ThreadLocal讓訪問某個(gè)變量的線程都擁有自己的局部變量,但是如果這個(gè)局部變量都指向同一個(gè)對(duì)象的話馍资,這個(gè)時(shí)候筒主,ThreadLocal就失效了。
ThreadLocal源碼剖析
ThreadLocal類的源碼在java.lang包中鸟蟹。其中主要有四個(gè)方法:
1. get()
// 返回當(dāng)前線程所對(duì)應(yīng)的線程變量
public T get() {
// 獲取當(dāng)前線程
Thread t = Thread.currentThread();
// 獲取當(dāng)前線程的成員變量 threadLocal
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();
}
從源碼中可以看到,get()方法首先通過當(dāng)前線程獲取所對(duì)應(yīng)的成員變量ThreadLocalMap使兔,然后通過ThreadLocalMap獲取當(dāng)前ThreadLocal的鍵值對(duì)Entry建钥,最后通過該Entry獲取目標(biāo)值result。
其中虐沥,getMap()方法可以獲取當(dāng)前線程所對(duì)應(yīng)的ThreadLocalMap熊经,其源代碼如下:
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
2. set(T value)
// 設(shè)置當(dāng)前線程的線程局部變量的值泽艘。
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
set方法首先獲取當(dāng)前線程所對(duì)應(yīng)的ThreadLocalMap,如果不為空镐依,則調(diào)用ThreadLocalMap的set()方法匹涮,key就是當(dāng)前ThreadLocal,如果不存在槐壳,則調(diào)用createMap()方法新建一個(gè)然低,其源代碼如下:
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
3. initialValue()
// 返回該線程局部變量的初始值。
protected T initialValue() {
return null;
}
該方法定義為protected級(jí)別且返回為null务唐,很明顯是要子類重寫來實(shí)現(xiàn)它的雳攘,所以我們?cè)谑褂肨hreadLocal的時(shí)候一般都應(yīng)該覆蓋該方法。該方法不能顯示調(diào)用枫笛,只有在第一次調(diào)用get()或者set()方法時(shí)才會(huì)被執(zhí)行吨灭,并且僅執(zhí)行1次。
4. remove()
// 將當(dāng)前線程局部變量的值刪除
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
該方法的目的是減少內(nèi)存占用刑巧,避免出現(xiàn)因?yàn)榫€程遲遲未結(jié)束而導(dǎo)致內(nèi)存泄漏的情況枝嘶。需要指出的是,當(dāng)線程結(jié)束后闽寡,對(duì)應(yīng)該線程的局部變量將自動(dòng)被垃圾回收页屠,所以顯式調(diào)用該方法清除線程的局部變量并不是必須的操作,但它可以加快內(nèi)存回收的速度特幔。
ThreadLocalMap類
從ThreadLocal的源碼中我們可以看到咨演,ThreadLocal的實(shí)現(xiàn)比較簡(jiǎn)單,主要是依賴于ThreadLocalMap這個(gè)類蚯斯,我們有必要好好理解一下后者薄风。
根據(jù)命名就可以看出,ThreadLocalMap拍嵌,它實(shí)際上是一個(gè)Map鍵值對(duì)遭赂。在其內(nèi)部使用了Entry的方式來實(shí)現(xiàn)key-value的存儲(chǔ):
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
在上面的代碼中,Entry內(nèi)的Key就是ThreadLocal横辆,而Value就是線程私有的那個(gè)變量撇他。同時(shí),Entry也繼承WeakReference狈蚤,所以說Entry所對(duì)應(yīng)key(ThreadLocal實(shí)例)的引用是一個(gè)弱引用困肩。
下面來看一下ThreadLocalMap類中幾個(gè)核心的方法:
1. set(ThreadLocal<?> key, Object value)
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;
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
if (k == key) {
e.value = value;
return;
}
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
源碼的意思簡(jiǎn)單明了,根據(jù)要保存的key到Entry數(shù)組中去匹配脆侮,如果key已經(jīng)存在就更新值锌畸,否則創(chuàng)建新的entry寫入。
值得注意的是靖避,這里的set()操作和我們?cè)诩螹ap了解的put()方式有點(diǎn)兒不一樣潭枣,雖然他們都是key-value結(jié)構(gòu)比默,不同點(diǎn)在于他們解決散列沖突的方式不同。 集合Map的put()采用的是拉鏈法盆犁,即在每個(gè)數(shù)組元素的位置命咐,存入鏈表來解決沖突。而ThreadLocalMap的set()則是采用開放定址法來解決沖突的谐岁。
set()操作除了存儲(chǔ)元素外醋奠,還有一個(gè)很重要的作用,就是replaceStaleEntry()和cleanSomeSlots()翰铡,這兩個(gè)方法可以清除掉key == null 的實(shí)例钝域,防止內(nèi)存泄漏。在set()方法中還有一個(gè)變量很重要:threadLocalHashCode锭魔,定義如下:
private final int threadLocalHashCode = nextHashCode();
從名字上面我們可以看出threadLocalHashCode應(yīng)該是ThreadLocal的散列值例证,定義為final,表示ThreadLocal一旦創(chuàng)建其散列值就已經(jīng)確定了迷捧,生成過程則是調(diào)用nextHashCode():
private static AtomicInteger nextHashCode = new AtomicInteger();
private static final int HASH_INCREMENT = 0x61c88647;
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
nextHashCode表示分配下一個(gè)ThreadLocal實(shí)例的threadLocalHashCode的值织咧,HASH_INCREMENT則表示分配兩個(gè)ThradLocal實(shí)例的threadLocalHashCode的增量,從nextHashCode就可以看出他們的定義漠秋。
2. getEntry(ThreadLocal<?> key)
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
return e;
else
return getEntryAfterMiss(key, i, e);
}
由于采用了開放定址法笙蒙,所以當(dāng)前key的散列值和元素在數(shù)組的索引并不是完全對(duì)應(yīng)的,首先取一個(gè)探測(cè)數(shù)(key的散列值)庆锦,如果所對(duì)應(yīng)的key就是我們要找的元素捅位,則返回,否則調(diào)用getEntryAfterMiss()再尋找搂抒,源碼如下:
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;
}
這里有一個(gè)重要的地方艇搀,當(dāng)key == null時(shí),調(diào)用了expungeStaleEntry()方法求晶,該方法用于處理key == null焰雕,有利于GC回收,能夠有效地避免內(nèi)存泄漏芳杏。
ThreadLocal與內(nèi)存泄漏
(注:本節(jié)參考了博文 http://blog.xiaohansong.com/2016/08/06/ThreadLocal-memory-leak/)
前面提到過矩屁,每個(gè)Thread都有一個(gè)ThreadLocal.ThreadLocalMap,該map的key為ThreadLocal實(shí)例的一個(gè)弱引用爵赵,我們知道弱引用有利于GC回收吝秕。當(dāng)ThreadLocal的key == null時(shí),GC就會(huì)回收這部分空間亚再,但是value卻不一定能夠被回收郭膛。
如果當(dāng)前線程再遲遲不結(jié)束的話,這些key為null的Entry的value就會(huì)一直存在一條強(qiáng)引用鏈:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永遠(yuǎn)無法回收氛悬,造成內(nèi)存泄漏则剃。
其實(shí),ThreadLocal類的設(shè)計(jì)中已經(jīng)考慮到這種情況如捅,也加上了一些防護(hù)措施:在觸發(fā)ThreadLocal的remove()時(shí)會(huì)清除線程ThreadLocalMap里key為null的value棍现。