Java并發(fā)編程之ThreadLocal詳解

ThreadLocal是什么?


ThreadLocal是一個(gè)關(guān)于創(chuàng)建線程局部變量的類做裙。

通常情況下匙隔,我們創(chuàng)建的變量是可以被任何一個(gè)線程訪問并修改的。而使用ThreadLocal創(chuàng)建的變量只能被當(dāng)前線程訪問阁簸,其他線程則無法訪問和修改。

ThreadLocal使用示例


示例1:ThreadLocal聲明基本類型變量

示例1 代碼段(1)

示例1 代碼段(2)

??執(zhí)行程序哼丈,可以得到:


示例1 執(zhí)行結(jié)果

從運(yùn)行結(jié)果可以看出启妹,對(duì)于基本類型變量,ThreadLocal確實(shí)是可以達(dá)到線程隔離作用的醉旦。

示例2:ThreadLocal聲明自定義類型的對(duì)象

示例2 自定義類型

示例2 代碼段(1)

示例2 代碼段(2)

??執(zhí)行程序饶米,可以得到:


示例2 運(yùn)行結(jié)果

從運(yùn)行結(jié)果可以看出,對(duì)于自定義類型的對(duì)象车胡,ThreadLocal也是可以達(dá)到線程隔離作用的檬输。

示例3:ThreadLocal聲明的變量都指向同一個(gè)對(duì)象

示例3 對(duì)程序代碼稍作修改

對(duì)示例2的代碼稍作修改,使得ThreadLocal聲明的變量初始化時(shí)不再實(shí)例化一個(gè)新的對(duì)象匈棘,而是讓它指向同一個(gè)對(duì)象丧慈,運(yùn)行查看結(jié)果:


示例3 運(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/)

ThreadLocal實(shí)現(xiàn)原理

前面提到過矩屁,每個(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棍现。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市镜遣,隨后出現(xiàn)的幾起案子己肮,更是在濱河造成了極大的恐慌,老刑警劉巖悲关,帶你破解...
    沈念sama閱讀 221,888評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件谎僻,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡寓辱,警方通過查閱死者的電腦和手機(jī)艘绍,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,677評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來秫筏,“玉大人诱鞠,你說我怎么就攤上這事≌饩矗” “怎么了航夺?”我有些...
    開封第一講書人閱讀 168,386評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)崔涂。 經(jīng)常有香客問我阳掐,道長(zhǎng),這世上最難降的妖魔是什么冷蚂? 我笑而不...
    開封第一講書人閱讀 59,726評(píng)論 1 297
  • 正文 為了忘掉前任缭保,我火速辦了婚禮,結(jié)果婚禮上帝雇,老公的妹妹穿的比我還像新娘涮俄。我一直安慰自己,他們只是感情好尸闸,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,729評(píng)論 6 397
  • 文/花漫 我一把揭開白布彻亲。 她就那樣靜靜地躺著,像睡著了一般吮廉。 火紅的嫁衣襯著肌膚如雪苞尝。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,337評(píng)論 1 310
  • 那天宦芦,我揣著相機(jī)與錄音宙址,去河邊找鬼。 笑死调卑,一個(gè)胖子當(dāng)著我的面吹牛抡砂,可吹牛的內(nèi)容都是我干的大咱。 我是一名探鬼主播,決...
    沈念sama閱讀 40,902評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼注益,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼碴巾!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起丑搔,我...
    開封第一講書人閱讀 39,807評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤厦瓢,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后啤月,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體煮仇,經(jīng)...
    沈念sama閱讀 46,349評(píng)論 1 318
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,439評(píng)論 3 340
  • 正文 我和宋清朗相戀三年谎仲,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了浙垫。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,567評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡强重,死狀恐怖绞呈,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情间景,我是刑警寧澤佃声,帶...
    沈念sama閱讀 36,242評(píng)論 5 350
  • 正文 年R本政府宣布,位于F島的核電站倘要,受9級(jí)特大地震影響圾亏,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜封拧,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,933評(píng)論 3 334
  • 文/蒙蒙 一志鹃、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧泽西,春花似錦曹铃、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,420評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至味抖,卻和暖如春评甜,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背仔涩。 一陣腳步聲響...
    開封第一講書人閱讀 33,531評(píng)論 1 272
  • 我被黑心中介騙來泰國(guó)打工忍坷, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,995評(píng)論 3 377
  • 正文 我出身青樓佩研,卻偏偏與公主長(zhǎng)得像柑肴,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子韧骗,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,585評(píng)論 2 359

推薦閱讀更多精彩內(nèi)容

  • 前言 ThreadLocal很多同學(xué)都搞不懂是什么東西嘉抒,可以用來干嘛零聚。但面試時(shí)卻又經(jīng)常問到袍暴,所以這次我和大家一起學(xué)...
    liangzzz閱讀 12,466評(píng)論 14 228
  • Android Handler機(jī)制系列文章整體內(nèi)容如下: Android Handler機(jī)制1之ThreadAnd...
    隔壁老李頭閱讀 7,645評(píng)論 4 30
  • 原創(chuàng)文章&經(jīng)驗(yàn)總結(jié)&從校招到A廠一路陽光一路滄桑 詳情請(qǐng)戳www.codercc.com 1. ThreadLoc...
    你聽___閱讀 6,741評(píng)論 8 19
  • 1. 概念 ThreadLocal 用于提供線程局部變量,在多線程環(huán)境可以保證各個(gè)線程里的變量獨(dú)立于其它線程里的變...
    zly394閱讀 1,757評(píng)論 0 1
  • 從三月份找實(shí)習(xí)到現(xiàn)在隶症,面了一些公司政模,掛了不少,但最終還是拿到小米蚂会、百度淋样、阿里、京東胁住、新浪趁猴、CVTE、樂視家的研發(fā)崗...
    時(shí)芥藍(lán)閱讀 42,277評(píng)論 11 349