ThreadLocal源碼解析

引言


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?。?!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容