引言
ThreadLocal,線程變量,線程可以將本次線程內(nèi)經(jīng)常使用的變量存儲(chǔ)到ThreadLocal中,方便本次線程內(nèi)其他的操作使用。
注:特別需要注意的是,有些博客說(shuō)ThreadLocal可以保證線程安全,這是錯(cuò)誤的認(rèn)識(shí),ThreadLocal存儲(chǔ)的只是每一個(gè)線程的本地變量,并未涉及到臨界區(qū),不能保證線程安全。在使用的時(shí)候一定要注意使用場(chǎng)景,ThreadLocal存儲(chǔ)的應(yīng)該是每個(gè)線程內(nèi)部共享的一些數(shù)據(jù),而非臨界區(qū)數(shù)據(jù)。
ThreadLocal源碼解析
用過(guò)ThreadLocal的程序員們可能都知道,ThreadLocal最常用的三個(gè)方法無(wú)非就是get(),set(T value)和remove(),本文就這三個(gè)常用的方法來(lái)分析ThreadLocal的相關(guān)源碼。
ThreadLocalMap
在分析具體操作源碼之前,先看看ThreadLocal底層的存儲(chǔ)結(jié)構(gòu)ThreadLocalLocalMap吧,這里著重講解它的get/set/remove操作,關(guān)于其他部分的源碼,有興趣的讀者可以自行閱讀。
- get操作
private Entry getEntry(ThreadLocal<?> key) {
//找到key在table中的位置
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
//找到的值是當(dāng)前查詢的key的值,返回
if (e != null && e.get() == key)
return e;
else
return getEntryAfterMiss(key, i, e);}
從上面的源碼可以看出,假如直接hash找到了對(duì)應(yīng)的slot,返回即可,假若沒(méi)找到呢。。。沒(méi)找到執(zhí)行下面的操作:
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
//在table里查詢,如若找到待查詢entry,返回該entry
while (e != null) {
ThreadLocal<?> k = e.get();
//entry的key為待查詢key
if (k == key)
return e;
//entry的key為null時(shí),表明當(dāng)前的entry并不是最新的,需要做相關(guān)的操作刪除掉非最新的entry
if (k == null)
expungeStaleEntry(i);
else
//i++,i == len 時(shí) i = 0
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
- set操作
//set操作,key為當(dāng)前ThreadLocal對(duì)象,value為對(duì)應(yīng)的變量值
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
//hash出當(dāng)前key所在slot
int i = key.threadLocalHashCode & (len-1);
//處理tab[i] != null
for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
//key存在,覆蓋value
if (k == key) {
e.value = value;
return;
}
//key不存在,需要在table對(duì)應(yīng)位置插入entry
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
//tab[i] == null,直接做插入
tab[i] = new Entry(key, value);
int sz = ++size;
//sz >= len * loadFactor(loadFactor = 2/3)需要resize
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
- remove操作
private void remove(ThreadLocal key) {
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)]) {
//找到對(duì)應(yīng)的entry
if (e.get() == key) {
//clear entry
e.clear();
//刪掉該entry
expungeStaleEntry(i);
return;
}
}
}
ThreadLocalMap的相關(guān)重要操作到這里就分析完畢,ThreadLocal的相關(guān)操作都是在ThreadLocalMap操作基礎(chǔ)上封裝的。
ThreadLocal.get()
public T get() {
Thread t = Thread.currentThread();
//獲取當(dāng)前線程的threadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null) {
//entry不為null,返回對(duì)應(yīng)的value
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null)
return (T)e.value;
}
//否則,返回null
return setInitialValue();
}
ThreadLocal.set(T value)
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
//當(dāng)前線程的threadLocalMap已創(chuàng)建,直接往map中set值
if (map != null)
map.set(this, value);
//當(dāng)前線程的threadLocalMap未創(chuàng)建,需要先創(chuàng)建再set
else
createMap(t, value);
}
ThreadLocal.remove()
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
后記
到這里為止,ThreadLocal的相關(guān)源碼分析就結(jié)束了。在最后還需要講講之前我在使用ThreadLocal遇到的坑。
在業(yè)務(wù)線工作的時(shí)候,發(fā)現(xiàn)很多時(shí)候都有線程內(nèi)共享變量的需求,ThreadLocal最適合這個(gè)場(chǎng)景,但是在使用ThreadLocal的時(shí)候,總是忘記remove,終于出現(xiàn)了一次巨大的故障。。。ThreadLocal和ThreadPoolExcutor一起使用時(shí),出現(xiàn)了內(nèi)存泄漏,在那一剎那,機(jī)器內(nèi)存使用率蹭蹭往上漲,排查了很久,才發(fā)現(xiàn)是ThreadLocal忘記remove了,為什么ThreadLocal不remove會(huì)造成內(nèi)存泄漏呢?
ThreadLocal導(dǎo)致的內(nèi)存泄漏
之前查過(guò)很多資料,都是ThreadLocal不remove不會(huì)導(dǎo)致內(nèi)存泄漏,但是事實(shí)卻不是如此,血一般的教訓(xùn)啊。
每個(gè)Thread都有一個(gè)ThreadLocalMap,雖說(shuō)它是弱引用,但是弱引用僅僅都是針對(duì)key,每個(gè)key都弱引用指向ThreadLocal。當(dāng)把ThreadLocal實(shí)例置為null的時(shí)候,沒(méi)有任何的強(qiáng)引用指向ThreadLocal實(shí)例,ThreadLocal將會(huì)被gc回收,但是,value卻不能被回收,因?yàn)榇嬖谝粭l從當(dāng)前線程連接過(guò)來(lái)的強(qiáng)引用,只有當(dāng)前 thread結(jié)束以后,thread,map,value將會(huì)全部被gc回收。
針對(duì)這個(gè)問(wèn)題,ThreadLocal為了減少內(nèi)存泄漏的機(jī)會(huì),在get/set的時(shí)候都會(huì)檢查ThreadLocalMap中是否有key == null的value,會(huì)把這部分value刪除掉。程序員們可能這時(shí)候有這個(gè)疑惑,既然都這么做來(lái)避免了,為什么會(huì)出現(xiàn)內(nèi)存泄漏???但是,大家就沒(méi)想過(guò)么,假若線程不被銷毀,它的ThreadLocal的get/set也一直未被使用,這不就出現(xiàn)了實(shí)際意義上的內(nèi)存泄漏么?剛好線程池里的一部分核心線程是不會(huì)被銷毀的,假若出現(xiàn)這種情況是一定會(huì)出現(xiàn)內(nèi)存泄漏的?。?!所以,在使用ThreadLocal結(jié)束之后一定不要忘記remove?。?!