深入理解ThreadLocal(轉(zhuǎn))

轉(zhuǎn)自 https://my.oschina.net/clopopo/blog/149368#comment-list
擴(kuò)展閱讀:http://qifuguang.me/2015/09/02/[Java并發(fā)包學(xué)習(xí)七]解密ThreadLocal/#comments 解讀ThreadLocal是否會(huì)引發(fā)內(nèi)存泄露
學(xué)習(xí)一個(gè)東西首先要知道為什么要引入它专挪,就是我們能用它來干什么涯保。所以我們先來看看ThreadLocal對(duì)我們到底有什么用鞋真,然后再來看看它的實(shí)現(xiàn)原理夜矗。

ThreadLocal如果單純從名字上來看像是“本地線程"這么個(gè)意思恩敌,只能說這個(gè)名字起的確實(shí)不太好,很容易讓人產(chǎn)生誤解羡儿,ThreadLocalVariable(線程本地變量)應(yīng)該是個(gè)更好的名字败匹。我們先看一下官方對(duì)ThreadLocal的描述:

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

我們從中摘出要點(diǎn):
1汽纠、每個(gè)線程都有自己的局部變量
每個(gè)線程都有一個(gè)獨(dú)立于其他線程的上下文來保存這個(gè)變量逛薇,一個(gè)線程的本地變量對(duì)其他線程是不可見的(有前提,后面解釋)
2疏虫、獨(dú)立于變量的初始化副本
ThreadLocal可以給一個(gè)初始值永罚,而每個(gè)線程都會(huì)獲得這個(gè)初始化值的一個(gè)副本啤呼,這樣才能保證不同的線程都有一份拷貝。
3呢袱、狀態(tài)與某一個(gè)線程相關(guān)聯(lián)
ThreadLocal 不是用于解決共享變量的問題的官扣,不是為了協(xié)調(diào)線程同步而存在,而是為了方便每個(gè)線程處理自己的狀態(tài)而引入的一個(gè)機(jī)制羞福,理解這點(diǎn)對(duì)正確使用ThreadLocal至關(guān)重要惕蹄。
我們先看一個(gè)簡(jiǎn)單的例子:

public class ThreadLocalTest {
        
        //創(chuàng)建一個(gè)Integer型的線程本地變量
    public static final ThreadLocal<Integer> local = new ThreadLocal<Integer>() {
        @Override
        protected Integer initialValue() {
            return 0;
        }
    };
    public static void main(String[] args) throws InterruptedException {
        Thread[] threads = new Thread[5];
        for (int j = 0; j < 5; j++) {       
               threads[j] = new Thread(new Runnable() {
                @Override
                public void run() {
                                        //獲取當(dāng)前線程的本地變量,然后累加5次
                    int num = local.get();
                    for (int i = 0; i < 5; i++) {
                        num++;
                    }
                                        //重新設(shè)置累加后的本地變量
                    local.set(num);
                    System.out.println(Thread.currentThread().getName() + " : "+ local.get());

                }
            }, "Thread-" + j);
        }

        for (Thread thread : threads) {
            thread.start();
        }
    }
}

運(yùn)行后結(jié)果:Thread-0 : 5Thread-4 : 5Thread-2 : 5Thread-1 : 5Thread-3 : 5
我們看到治专,每個(gè)線程累加后的結(jié)果都是5卖陵,各個(gè)線程處理自己的本地變量值,線程之間互不影響张峰。
我們?cè)賮砜匆粋€(gè)例子:

public class ThreadLocalTest {
    private static Index num = new Index();
        //創(chuàng)建一個(gè)Index類型的本地變量 
    private static ThreadLocal<Index> local = new ThreadLocal<Index>() {
        @Override
        protected Index initialValue() {
            return num;
        }
    };

    public static void main(String[] args) throws InterruptedException {
        Thread[] threads = new Thread[5];
        for (int j = 0; j < 5; j++) {
            threads[j] = new Thread(new Runnable() {
                @Override
                public void run() {
                                        //取出當(dāng)前線程的本地變量泪蔫,并累加1000次
                    Index index = local.get();
                    for (int i = 0; i < 1000; i++) {                                          
                        index.increase();
                    }
                    System.out.println(Thread.currentThread().getName() + " : "+ index.num);

                }
            }, "Thread-" + j);
        }
        for (Thread thread : threads) {
            thread.start();
        }
    }

    static class Index {
        int num;

        public void increase() {
            num++;
        }
    }
}

執(zhí)行后我們發(fā)現(xiàn)結(jié)果如下(每次運(yùn)行還都不一樣):
Thread-0 : 1390Thread-2 : 2390Thread-4 : 4390Thread-3 : 3491Thread-1 : 1390
這次為什么線程本地變量又失效了呢?大家可以仔細(xì)觀察上面代碼自己先找一下原因喘批。
-----------------------------------------------低調(diào)的分割線-------------------------------------------
讓我們?cè)賮砘匚兑幌?“ThreadLocal可以給一個(gè)初始值撩荣,而每個(gè)線程都會(huì)獲得這個(gè)初始化值的一個(gè)副本” 這句話∪纳睿“初始值的副本餐曹。。敌厘√ê铮”,貌似想起點(diǎn)什么俱两。我們?cè)賮砜匆幌律厦娲a中定義ThreadLocal的地方

private static Index num = new Index();
    private static ThreadLocal<Index> local = new ThreadLocal<Index>() {
        @Override
        protected Index initialValue() {
            return num;       // 注意這里卿吐,返回的是已經(jīng)定義好的對(duì)象num,而不是new Index()
        }
    };```
 上面代碼中锋华,我們通過覆蓋initialValue函數(shù)來給我們的ThreadLocal提供初始值嗡官,每個(gè)線程都會(huì)獲取這個(gè)初始值的一個(gè)副本。而現(xiàn)在我們的初始值是一個(gè)定義好的一個(gè)對(duì)象毯焕,num是這個(gè)對(duì)象的引用.換句話說我們的初始值是一個(gè)引用衍腥。引用的副本和引用指向的不就是同一個(gè)對(duì)象嗎?
![](http://upload-images.jianshu.io/upload_images/1986284-3692cf9ec8aeb0b8.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
如果我們想給每一個(gè)線程都保存一個(gè)Index對(duì)象應(yīng)該怎么辦呢纳猫?那就是創(chuàng)建對(duì)象的副本而不是對(duì)象引用的副本:

private static ThreadLocal local = new ThreadLocal() {
@Override
protected Index initialValue() {
return new Index(); //注意這里
}
};


對(duì)象的拷貝圖示:
  ![](http://upload-images.jianshu.io/upload_images/1986284-bf68cec71dd77b99.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
現(xiàn)在我們應(yīng)該能明白ThreadLocal本地變量的含義了吧婆咸。接下來我們就來看看ThreadLocal的源碼,從內(nèi)部來揭示它的神秘面紗芜辕。
ThreadLocal有一個(gè)內(nèi)部類ThreadLocalMap,這個(gè)類的實(shí)現(xiàn)占了整個(gè)ThreadLocal類源碼的一多半尚骄。這個(gè)ThreadLocalMap的作用非常關(guān)鍵,它就是線程真正保存線程自己本地變量的容器侵续。每一個(gè)線程都有自己的單獨(dú)的一個(gè)ThreadLocalMap實(shí)例倔丈,其所有的本地變量都會(huì)保存到這一個(gè)map中『┤颍現(xiàn)在就讓我們從ThreadLocal的get和set這兩個(gè)最常用的方法開始分析:

public T get() {
//獲取當(dāng)前執(zhí)行線程
Thread t = Thread.currentThread();
//取得當(dāng)前線程的ThreadLocalMap實(shí)例
ThreadLocalMap map = getMap(t);
//如果map不為空,說明該線程已經(jīng)有了一個(gè)ThreadLocalMap實(shí)例
if (map != null) {
//map中保存線程的所有的線程本地變量需五,我們要去查找當(dāng)前線程本地變量
ThreadLocalMap.Entry e = map.getEntry(this);
//如果當(dāng)前線程本地變量存在這個(gè)map中鹉动,則返回其對(duì)應(yīng)的值
if (e != null)
return (T)e.value;
}
//如果map不存在或者map中不存在當(dāng)前線程本地變量,返回初始值
return setInitialValue();
}```
強(qiáng)調(diào)一下:Thread對(duì)象都有一個(gè)ThreadLocalMap類型的屬性threadLocals宏邮,這個(gè)屬性是專門用于保存自己所有的線程本地變量的泽示。這個(gè)屬性在線程對(duì)象初始化的時(shí)候?yàn)閚ull。所以對(duì)一個(gè)線程對(duì)象第一次使用線程本地變量的時(shí)候蜜氨,需要對(duì)這個(gè)threadLocals屬性進(jìn)行初始化操作械筛。注意要區(qū)別 “線程第一次使用本地線程變量”和“第一次使用某一個(gè)線程本地線程變量”。
getMap方法:

//直接返回線程對(duì)象的threadLocals屬性
ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }```
setInitialValue方法:(看完后再回顧一下之前的那個(gè)例子)

private T setInitialValue() {
//獲取初始化值飒炎,initialValue 就是我們之前覆蓋的方法
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
//如果map不為空埋哟,將初始化值放入到當(dāng)前線程的ThreadLocalMap對(duì)象中
if (map != null)
map.set(this, value);
else
//當(dāng)前線程第一次使用本地線程變量,需要對(duì)map進(jìn)行初始化工作
createMap(t, value);
//返回初始化值
return value;
}```
我們?cè)賮砜匆幌聅et方法

public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);

        if (map != null)
            map.set(this, value);
        //說明線程第一次使用線程本地變量(注意這里的第一次含義)
        else
            createMap(t, value);
    }```
ThradLocal還有一個(gè)remove方法厌丑,讓我們來分析一下:

public void remove() {
//獲取當(dāng)前線程的ThreadLocalMap對(duì)象
ThreadLocalMap m = getMap(Thread.currentThread());
//如果map不為空定欧,則刪除該本地變量的值
if (m != null)
m.remove(this);
}```
到這里大家應(yīng)該對(duì)ThreadLocal變量比較清晰了渔呵,至于ThradLocalMap的實(shí)現(xiàn)細(xì)節(jié)這里就不在說了怒竿。大家有興趣可以自己去看ThreadLocal的源碼。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末扩氢,一起剝皮案震驚了整個(gè)濱河市耕驰,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌录豺,老刑警劉巖朦肘,帶你破解...
    沈念sama閱讀 218,036評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異双饥,居然都是意外死亡媒抠,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,046評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門咏花,熙熙樓的掌柜王于貴愁眉苦臉地迎上來趴生,“玉大人,你說我怎么就攤上這事昏翰〔源遥” “怎么了?”我有些...
    開封第一講書人閱讀 164,411評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵棚菊,是天一觀的道長浸踩。 經(jīng)常有香客問我,道長统求,這世上最難降的妖魔是什么检碗? 我笑而不...
    開封第一講書人閱讀 58,622評(píng)論 1 293
  • 正文 為了忘掉前任据块,我火速辦了婚禮,結(jié)果婚禮上后裸,老公的妹妹穿的比我還像新娘瑰钮。我一直安慰自己,他們只是感情好微驶,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,661評(píng)論 6 392
  • 文/花漫 我一把揭開白布浪谴。 她就那樣靜靜地躺著,像睡著了一般因苹。 火紅的嫁衣襯著肌膚如雪苟耻。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,521評(píng)論 1 304
  • 那天扶檐,我揣著相機(jī)與錄音凶杖,去河邊找鬼。 笑死款筑,一個(gè)胖子當(dāng)著我的面吹牛智蝠,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播奈梳,決...
    沈念sama閱讀 40,288評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼杈湾,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了攘须?” 一聲冷哼從身側(cè)響起漆撞,我...
    開封第一講書人閱讀 39,200評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎于宙,沒想到半個(gè)月后浮驳,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,644評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡捞魁,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,837評(píng)論 3 336
  • 正文 我和宋清朗相戀三年至会,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片谱俭。...
    茶點(diǎn)故事閱讀 39,953評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡奉件,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出旺上,到底是詐尸還是另有隱情瓶蚂,我是刑警寧澤,帶...
    沈念sama閱讀 35,673評(píng)論 5 346
  • 正文 年R本政府宣布宣吱,位于F島的核電站窃这,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜杭攻,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,281評(píng)論 3 329
  • 文/蒙蒙 一祟敛、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧兆解,春花似錦馆铁、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,889評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至现拒,卻和暖如春辣垒,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背印蔬。 一陣腳步聲響...
    開封第一講書人閱讀 33,011評(píng)論 1 269
  • 我被黑心中介騙來泰國打工勋桶, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人侥猬。 一個(gè)月前我還...
    沈念sama閱讀 48,119評(píng)論 3 370
  • 正文 我出身青樓例驹,卻偏偏與公主長得像,于是被迫代替她去往敵國和親退唠。 傳聞我的和親對(duì)象是個(gè)殘疾皇子鹃锈,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,901評(píng)論 2 355

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