【死磕Java并發(fā)】—–深入分析ThreadLocal

ThreadLoacal是什么斤蔓?

ThreadLocal是啥?以前面試別人時(shí)就喜歡問這個(gè)培愁,有些伙伴喜歡把它和線程同步機(jī)制混為一談监氢,事實(shí)上ThreadLocal與線程同步無關(guān)。ThreadLocal雖然提供了一種解決多線程環(huán)境下成員變量的問題衅码,但是它并不是解決多線程共享變量的問題拯刁。那么ThreadLocal到底是什么呢?

API是這樣介紹它的:
This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its {@code get} or {@code set} method) has its own, independently initialized copy of the variable. {@code ThreadLocal} instances are typically private static fields in classes that wish to associate state with a thread (e.g.,a user ID or Transaction ID).

該類提供了線程局部 (thread-local) 變量逝段。這些變量不同于它們的普通對(duì)應(yīng)物垛玻,因?yàn)樵L問某個(gè)變量(通過其getset 方法)的每個(gè)線程都有自己的局部變量,它獨(dú)立于變量的初始化副本奶躯。ThreadLocal實(shí)例通常是類中的 private static 字段帚桩,它們希望將狀態(tài)與某一個(gè)線程(例如,用戶 ID 或事務(wù) ID)相關(guān)聯(lián)嘹黔。

所以ThreadLocal與線程同步機(jī)制不同账嚎,線程同步機(jī)制是多個(gè)線程共享同一個(gè)變量,而ThreadLocal是為每一個(gè)線程創(chuàng)建一個(gè)單獨(dú)的變量副本儡蔓,故而每個(gè)線程都可以獨(dú)立地改變自己所擁有的變量副本郭蕉,而不會(huì)影響其他線程所對(duì)應(yīng)的副本∥菇可以說ThreadLocal為多線程環(huán)境下變量問題提供了另外一種解決思路召锈。

ThreadLocal定義了四個(gè)方法:

  • get():返回此線程局部變量的當(dāng)前線程副本中的值。
  • initialValue():返回此線程局部變量的當(dāng)前線程的“初始值”开呐。
  • remove():移除此線程局部變量當(dāng)前線程的值烟勋。
  • set(T value):將此線程局部變量的當(dāng)前線程副本中的值設(shè)置為指定值规求。

除了這四個(gè)方法,ThreadLocal內(nèi)部還有一個(gè)靜態(tài)內(nèi)部類ThreadLocalMap卵惦,該內(nèi)部類才是實(shí)現(xiàn)線程隔離機(jī)制的關(guān)鍵阻肿,get()、set()沮尿、remove()都是基于該內(nèi)部類操作丛塌。ThreadLocalMap提供了一種用鍵值對(duì)方式存儲(chǔ)每一個(gè)線程的變量副本的方法,key為當(dāng)前ThreadLocal對(duì)象畜疾,value則是對(duì)應(yīng)線程的變量副本赴邻。

對(duì)于ThreadLocal需要注意的有兩點(diǎn):
1. ThreadLocal實(shí)例本身是不存儲(chǔ)值,它只是提供了一個(gè)在當(dāng)前線程中找到副本值得key啡捶。
2. 是ThreadLocal包含在Thread中姥敛,而不是Thread包含在ThreadLocal中,有些小伙伴會(huì)弄錯(cuò)他們的關(guān)系瞎暑。

下圖是Thread彤敛、ThreadLocal、ThreadLocalMap的關(guān)系(http://blog.xiaohansong.com/2016/08/06/ThreadLocal-memory-leak/

image.png

ThreadLocal使用示例

示例如下:

public class SeqCount {

    private static ThreadLocal<Integer> seqCount = new ThreadLocal<Integer>(){
        // 實(shí)現(xiàn)initialValue()
        public Integer initialValue() {
            return 0;
        }
    };

    public int nextSeq(){
        seqCount.set(seqCount.get() + 1);

        return seqCount.get();
    }

    public static void main(String[] args){
        SeqCount seqCount = new SeqCount();

        SeqThread thread1 = new SeqThread(seqCount);
        SeqThread thread2 = new SeqThread(seqCount);
        SeqThread thread3 = new SeqThread(seqCount);
        SeqThread thread4 = new SeqThread(seqCount);

        thread1.start();
        thread2.start();
        thread3.start();
        thread4.start();
    }

    private static class SeqThread extends Thread{
        private SeqCount seqCount;

        SeqThread(SeqCount seqCount){
            this.seqCount = seqCount;
        }

        public void run() {
            for(int i = 0 ; i < 3 ; i++){
                System.out.println(Thread.currentThread().getName() + " seqCount :" + seqCount.nextSeq());
            }
        }
    }
}

運(yùn)行結(jié)果:

image.png

從運(yùn)行結(jié)果可以看出了赌,ThreadLocal確實(shí)是可以達(dá)到線程隔離機(jī)制墨榄,確保變量的安全性。這里我們想一個(gè)問題勿她,在上面的代碼中ThreadLocal的initialValue()方法返回的是0袄秩,加入該方法返回得是一個(gè)對(duì)象呢,會(huì)產(chǎn)生什么后果呢逢并?例如:

    A a = new A();
    private static ThreadLocal<A> seqCount = new ThreadLocal<A>(){
        // 實(shí)現(xiàn)initialValue()
        public A initialValue() {
            return a;
        }
    };

    class A{
        // ....
    }

具體過程請參考:對(duì)ThreadLocal實(shí)現(xiàn)原理的一點(diǎn)思考

ThreadLocal源碼解析

ThreadLocal雖然解決了這個(gè)多線程變量的復(fù)雜問題之剧,但是它的源碼實(shí)現(xiàn)卻是比較簡單的。ThreadLocalMap是實(shí)現(xiàn)ThreadLocal的關(guān)鍵砍聊,我們先從它入手猪狈。

ThreadLocalMap

ThreadLocalMap其內(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的key就是ThreadLocal辩恼,而value就是值。同時(shí)谓形,Entry也繼承WeakReference灶伊,所以說Entry所對(duì)應(yīng)key(ThreadLocal實(shí)例)的引用為一個(gè)弱引用(關(guān)于弱引用這里就不多說了,感興趣的可以關(guān)注這篇博客:Java 理論與實(shí)踐: 用弱引用堵住內(nèi)存泄漏

ThreadLocalMap的源碼稍微多了點(diǎn)寒跳,我們就看兩個(gè)最核心的方法getEntry()聘萨、set(ThreadLocal> key, Object value)方法。

set(ThreadLocal> key, Object value)

    private void set(ThreadLocal<?> key, Object value) {

        ThreadLocal.ThreadLocalMap.Entry[] tab = table;
        int len = tab.length;

        // 根據(jù) ThreadLocal 的散列值童太,查找對(duì)應(yīng)元素在數(shù)組中的位置
        int i = key.threadLocalHashCode & (len-1);

        // 采用“線性探測法”米辐,尋找合適位置
        for (ThreadLocal.ThreadLocalMap.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 == null,但是存在值(因?yàn)榇颂幍膃 != null)翘贮,說明之前的ThreadLocal對(duì)象已經(jīng)被回收了
            if (k == null) {
                // 用新元素替換陳舊的元素
                replaceStaleEntry(key, value, i);
                return;
            }
        }

        // ThreadLocal對(duì)應(yīng)的key實(shí)例不存在也沒有陳舊元素赊窥,new 一個(gè)
        tab[i] = new ThreadLocal.ThreadLocalMap.Entry(key, value);

        int sz = ++size;

        // cleanSomeSlots 清楚陳舊的Entry(key == null)
        // 如果沒有清理陳舊的 Entry 并且數(shù)組中的元素大于了閾值,則進(jìn)行 rehash
        if (!cleanSomeSlots(i, sz) && sz >= threshold)
            rehash();
    }

這個(gè)set()操作和我們在集合了解的put()方式有點(diǎn)兒不一樣狸页,雖然他們都是key-value結(jié)構(gòu)锨能,不同在于他們解決散列沖突的方式不同。集合Map的put()采用的是拉鏈法芍耘,而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就可以看出他們的定義。

getEntry()

        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è)探測數(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)存泄漏。

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) {
            // 從當(dāng)前線程的ThreadLocalMap獲取相對(duì)應(yīng)的Entry
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")

                // 獲取目標(biāo)值        
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }

首先通過當(dāng)前線程獲取所對(duì)應(yīng)的成員變量ThreadLocalMap宿亡,然后通過ThreadLocalMap獲取當(dāng)前ThreadLocal的Entry常遂,最后通過所獲取的Entry獲取目標(biāo)值result。

getMap()方法可以獲取當(dāng)前線程所對(duì)應(yīng)的ThreadLocalMap挽荠,如下:

    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

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);
    }

獲取當(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);
    }

initialValue()

返回該線程局部變量的初始值腹忽。

    protected T initialValue() {
        return null;
    }

該方法定義為protected級(jí)別且返回為null,很明顯是要子類實(shí)現(xiàn)它的砚作,所以我們在使用ThreadLocal的時(shí)候一般都應(yīng)該覆蓋該方法窘奏。該方法不能顯示調(diào)用,只有在第一次調(diào)用get()或者set()方法時(shí)才會(huì)被執(zhí)行葫录,并且僅執(zhí)行1次着裹。

remove()

將當(dāng)前線程局部變量的值刪除。

    public void remove() {
        ThreadLocalMap m = getMap(Thread.currentThread());
        if (m != null)
            m.remove(this);
    }

該方法的目的是減少內(nèi)存的占用米同。當(dāng)然骇扇,我們不需要顯示調(diào)用該方法,因?yàn)橐粋€(gè)線程結(jié)束后面粮,它所對(duì)應(yīng)的局部變量就會(huì)被垃圾回收少孝。

ThreadLocal為什么會(huì)內(nèi)存泄漏

前面提到每個(gè)Thread都有一個(gè)ThreadLocal.ThreadLocalMap的map,該map的key為ThreadLocal實(shí)例熬苍,它為一個(gè)弱引用稍走,我們知道弱引用有利于GC回收。當(dāng)ThreadLocal的key == null時(shí)柴底,GC就會(huì)回收這部分空間婿脸,但是value卻不一定能夠被回收,因?yàn)樗€與Current Thread存在一個(gè)強(qiáng)引用關(guān)系柄驻,如下(圖片來自http://www.reibang.com/p/ee8c9dccc953):

image.png

由于存在這個(gè)強(qiáng)引用關(guān)系狐树,會(huì)導(dǎo)致value無法回收。如果這個(gè)線程對(duì)象不會(huì)銷毀那么這個(gè)強(qiáng)引用關(guān)系則會(huì)一直存在鸿脓,就會(huì)出現(xiàn)內(nèi)存泄漏情況抑钟。所以說只要這個(gè)線程對(duì)象能夠及時(shí)被GC回收,就不會(huì)出現(xiàn)內(nèi)存泄漏野哭。如果碰到線程池味赃,那就更坑了。

那么要怎么避免這個(gè)問題呢虐拓?

在前面提過,在ThreadLocalMap中的setEntry()傲武、getEntry()蓉驹,如果遇到key == null的情況城榛,會(huì)對(duì)value設(shè)置為null。當(dāng)然我們也可以顯示調(diào)用ThreadLocal的remove()方法進(jìn)行處理态兴。

下面再對(duì)ThreadLocal進(jìn)行簡單的總結(jié):

  • ThreadLocal 不是用于解決共享變量的問題的狠持,也不是為了協(xié)調(diào)線程同步而存在,而是為了方便每個(gè)線程處理自己的狀態(tài)而引入的一個(gè)機(jī)制瞻润。這點(diǎn)至關(guān)重要喘垂。
  • 每個(gè)Thread內(nèi)部都有一個(gè)ThreadLocal.ThreadLocalMap類型的成員變量,該成員變量用來存儲(chǔ)實(shí)際的ThreadLocal變量副本绍撞。
  • ThreadLocal并不是為線程保存對(duì)象的副本正勒,它僅僅只起到一個(gè)索引的作用。它的主要目的是為每一個(gè)線程隔離一個(gè)類的實(shí)例傻铣,這個(gè)實(shí)例的作用范圍僅限于線程內(nèi)部章贞。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市非洲,隨后出現(xiàn)的幾起案子鸭限,更是在濱河造成了極大的恐慌,老刑警劉巖两踏,帶你破解...
    沈念sama閱讀 222,464評(píng)論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件败京,死亡現(xiàn)場離奇詭異,居然都是意外死亡梦染,警方通過查閱死者的電腦和手機(jī)赡麦,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,033評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來弓坞,“玉大人隧甚,你說我怎么就攤上這事《啥常” “怎么了戚扳?”我有些...
    開封第一講書人閱讀 169,078評(píng)論 0 362
  • 文/不壞的土叔 我叫張陵,是天一觀的道長族吻。 經(jīng)常有香客問我帽借,道長,這世上最難降的妖魔是什么超歌? 我笑而不...
    開封第一講書人閱讀 59,979評(píng)論 1 299
  • 正文 為了忘掉前任砍艾,我火速辦了婚禮,結(jié)果婚禮上巍举,老公的妹妹穿的比我還像新娘脆荷。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,001評(píng)論 6 398
  • 文/花漫 我一把揭開白布蜓谋。 她就那樣靜靜地躺著梦皮,像睡著了一般。 火紅的嫁衣襯著肌膚如雪桃焕。 梳的紋絲不亂的頭發(fā)上剑肯,一...
    開封第一講書人閱讀 52,584評(píng)論 1 312
  • 那天,我揣著相機(jī)與錄音观堂,去河邊找鬼让网。 笑死,一個(gè)胖子當(dāng)著我的面吹牛师痕,可吹牛的內(nèi)容都是我干的溃睹。 我是一名探鬼主播,決...
    沈念sama閱讀 41,085評(píng)論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼七兜,長吁一口氣:“原來是場噩夢啊……” “哼丸凭!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起腕铸,我...
    開封第一講書人閱讀 40,023評(píng)論 0 277
  • 序言:老撾萬榮一對(duì)情侶失蹤惜犀,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后狠裹,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體虽界,經(jīng)...
    沈念sama閱讀 46,555評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,626評(píng)論 3 342
  • 正文 我和宋清朗相戀三年涛菠,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了莉御。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,769評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡俗冻,死狀恐怖礁叔,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情迄薄,我是刑警寧澤琅关,帶...
    沈念sama閱讀 36,439評(píng)論 5 351
  • 正文 年R本政府宣布,位于F島的核電站讥蔽,受9級(jí)特大地震影響涣易,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜冶伞,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,115評(píng)論 3 335
  • 文/蒙蒙 一新症、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧响禽,春花似錦徒爹、人聲如沸荚醒。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,601評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽腌且。三九已至,卻和暖如春榛瓮,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背巫击。 一陣腳步聲響...
    開封第一講書人閱讀 33,702評(píng)論 1 274
  • 我被黑心中介騙來泰國打工禀晓, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人坝锰。 一個(gè)月前我還...
    沈念sama閱讀 49,191評(píng)論 3 378
  • 正文 我出身青樓粹懒,卻偏偏與公主長得像,于是被迫代替她去往敵國和親顷级。 傳聞我的和親對(duì)象是個(gè)殘疾皇子凫乖,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,781評(píng)論 2 361