Java并發(fā)編程:ThreadLocal的使用以及實(shí)現(xiàn)原理解析

前言

前面的文章里嗤详,我們學(xué)習(xí)了有關(guān)鎖的使用妈踊,鎖的機(jī)制是保證同一時(shí)刻只能有一個(gè)線程訪問(wèn)臨界區(qū)的資源,也就是通過(guò)控制資源的手段來(lái)保證線程安全彰亥,這固然是一種有效的手段咧七,但程序的運(yùn)行效率也因此大大降低。那么任斋,有沒(méi)有更好的方式呢猪叙?答案是有的,既然鎖是嚴(yán)格控制資源的方式來(lái)保證線程安全,那我們可以反其道而行之穴翩,增加更多資源犬第,保證每個(gè)線程都能得到所需對(duì)象,各自為營(yíng)芒帕,互不影響歉嗓,從而達(dá)到線程安全的目的,而ThreadLocal便是采用這樣的思路背蟆。

ThreadLocal實(shí)例

ThreadLocal翻譯成中文的話大概可以說(shuō)是:線程局部變量鉴分,也就是只有當(dāng)前線程能夠訪問(wèn)。它的設(shè)計(jì)作用是為每一個(gè)使用該變量的線程都提供一個(gè)變量值的副本带膀,每個(gè)線程都是改變自己的副本并且不會(huì)和其他線程的副本沖突志珍,這樣一來(lái),從線程的角度來(lái)看垛叨,就好像每個(gè)線程都擁有了該變量伦糯。

下面是一個(gè)簡(jiǎn)單的實(shí)例:

public class ThreadLocalDemo {

    static ThreadLocal<Integer> local = new ThreadLocal<Integer>(){
        @Override
        protected Integer initialValue() {
            return 0;
        }
    };

    public static class MyRunnable implements Runnable{

        @Override
        public void run() {
            for (int i = 0;i<3;i++){
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                int value = local.get();
                System.out.println(Thread.currentThread().getName() + ":" + value);
                local.set(value + 1);
            }
        }
    }

    public static void main(String[] args) {
        MyRunnable runnable = new MyRunnable();
        Thread t1 = new Thread(runnable);
        Thread t2 = new Thread(runnable);
        t1.start();
        t2.start();
    }
}

上面的代碼不難理解,首先是定義了一個(gè)名為 local 的ThreadLocal變量嗽元,并初識(shí)變量的值為0敛纲,然后是定義了一個(gè)實(shí)現(xiàn)Runnable接口的內(nèi)部類(lèi),在其run方法中對(duì)local 的值做讀取和加1的操作剂癌,最后是main方法中開(kāi)啟兩個(gè)線程來(lái)運(yùn)行內(nèi)部類(lèi)實(shí)例淤翔。

以上就是代碼的大概邏輯,運(yùn)行main函數(shù)后佩谷,程序的輸出結(jié)果如下:

Thread-0:0
Thread-1:0
Thread-1:1
Thread-0:1
Thread-1:2
Thread-0:2

從結(jié)果可以看出旁壮,雖然兩個(gè)線程都共用一個(gè)Runnable實(shí)例,但兩個(gè)線程中所展示的ThreadLocal的數(shù)據(jù)值并不會(huì)相互影響谐檀,也就是說(shuō)這種情況下的local 變量保存的數(shù)據(jù)相當(dāng)于是線程安全的寡具,只能被當(dāng)前線程訪問(wèn)。

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

那么ThreadLocal內(nèi)部是怎么保證對(duì)象是線程私有的呢稚补?毫無(wú)疑問(wèn)童叠,答案需要從源碼中查找】文唬回顧前面的代碼厦坛,可以發(fā)現(xiàn)其中調(diào)用了ThreadLocal的兩個(gè)方法setget,我們就從這兩個(gè)方法入手乍惊。

先看 set() 的源碼:

public void set(T value) {
    Thread t = Thread.currentThread();
    // 獲取線程的ThreadLocalMap杜秸,返回map
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        //map為空,創(chuàng)建
        createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

set的代碼邏輯比較簡(jiǎn)單润绎,主要是把值設(shè)置到當(dāng)前線程的一個(gè)ThreadLocalMap對(duì)象中撬碟,而ThreadLocalMap可以理解成一個(gè)Map诞挨,它是定義在Thread類(lèi)中內(nèi)部的成員,初始化是為null呢蛤,

ThreadLocal.ThreadLocalMap threadLocals = null;

不過(guò)惶傻,與常見(jiàn)的Map實(shí)現(xiàn)類(lèi),如HashMap之類(lèi)的不同的是其障,ThreadLocalMap中的Entry是繼承于WeakReference類(lèi)的银室,保持了對(duì) “鍵” 的弱引用和對(duì) “值” 的強(qiáng)引用,這是類(lèi)的源碼:

static class ThreadLocalMap {

    static class Entry extends WeakReference<ThreadLocal<?>> {
        /** The value associated with this ThreadLocal. */
        Object value;

        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
    }
    
    //省略剩下的源碼
    ....................
}

從源碼中中可以看出励翼,Entry構(gòu)造函數(shù)中的參數(shù) k 就是ThreadLocal實(shí)例蜈敢,調(diào)用super(k) 表明對(duì) k 是弱引用,使用弱引用的原因在于汽抚,當(dāng)沒(méi)有強(qiáng)引用指向 ThreadLocal 實(shí)例時(shí)抓狭,它可被回收,從而避免內(nèi)存泄露造烁,那么為何需要防止內(nèi)存泄露呢否过?原因下面會(huì)說(shuō)到。

接著說(shuō)set方法的邏輯膨蛮,當(dāng)調(diào)用set方法時(shí),其實(shí)是將數(shù)據(jù)寫(xiě)入threadLocals這個(gè)Map對(duì)象中季研,這個(gè)Map的key為T(mén)hreadLocal當(dāng)前對(duì)象敞葛,value就是我們存入的值。而threadLocals本身能保存多個(gè)ThreadLocal對(duì)象与涡,相當(dāng)于一個(gè)ThreadLocal集合惹谐。

接著看 get() 的源碼:

public T get() {
    Thread t = Thread.currentThread();
    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;
        }
    }
    //設(shè)置初識(shí)值到ThreadLocal中并返回
    return setInitialValue();
}
private T setInitialValue() {
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
        return value;
    }

get方法的邏輯也是比較簡(jiǎn)單的,就是直接獲取當(dāng)前線程的ThreadLocalMap對(duì)象驼卖,如果該對(duì)象不為空就返回它的value值氨肌,否則就把初始值設(shè)置到ThreadLocal中并返回。

看到這酌畜,我們大概就能明白為什么ThreadLocal能實(shí)現(xiàn)線程私有的原理了怎囚,其實(shí)就是每個(gè)線程都維護(hù)著一個(gè)ThreadLocal的容器,這個(gè)容器就是ThreadLocalMap桥胞,可以保存多個(gè)ThreadLocal對(duì)象恳守。而調(diào)用ThreadLocal的set或get方法其實(shí)就是對(duì)當(dāng)前線程的ThreadLocal變量操作,與其他線程是分開(kāi)的贩虾,所以才能保證線程私有催烘,也就不存在線程安全的問(wèn)題了。

然而缎罢,該方案雖然能保證線程私有伊群,但卻會(huì)占用大量的內(nèi)存考杉,因?yàn)槊總€(gè)線程都維護(hù)著一個(gè)Map,當(dāng)訪問(wèn)某個(gè)ThreadLocal變量后舰始,線程會(huì)在自己的Map內(nèi)維護(hù)該ThreadLocal變量與具體實(shí)現(xiàn)的映射崇棠,如果這些映射一直存在,就表明ThreadLocal 存在引用的情況蔽午,那么系統(tǒng)GC就無(wú)法回收這些變量易茬,可能會(huì)造成內(nèi)存泄露。

針對(duì)這種情況及老,上面所說(shuō)的ThreadLocalMap中Entry的弱引用就起作用了抽莱。

TheadLocal與同步機(jī)制的區(qū)別

最后,總結(jié)一下ThreadLocal和同步機(jī)制之間的區(qū)別吧骄恶。

實(shí)現(xiàn)機(jī)制:

同步機(jī)制采用了“以時(shí)間換空間”的方式食铐,控制資源保證同一時(shí)刻只能有一個(gè)線程訪問(wèn)。

ThreadLocal采用了“以空間換時(shí)間”的方式僧鲁,為每一個(gè)線程都提供一份變量的副本虐呻,從而實(shí)現(xiàn)同時(shí)訪問(wèn)而互不影響,但因?yàn)槊總€(gè)線程都維護(hù)著一份副本寞秃,對(duì)內(nèi)存空間的占用會(huì)增加斟叼。

數(shù)據(jù)共享:

同步機(jī)制是對(duì)公共資源做控制訪問(wèn)的方式來(lái)保證線程安全,但資源仍是共享狀態(tài)春寿,可用于線程間的通信朗涩;

ThreadLocal是每個(gè)線程都有自己的資源(變量)副本,互相之間不影響绑改,也就不存在共享的說(shuō)法了谢床。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市厘线,隨后出現(xiàn)的幾起案子识腿,更是在濱河造成了極大的恐慌,老刑警劉巖造壮,帶你破解...
    沈念sama閱讀 211,265評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件渡讼,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡耳璧,警方通過(guò)查閱死者的電腦和手機(jī)硝全,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,078評(píng)論 2 385
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)楞抡,“玉大人伟众,你說(shuō)我怎么就攤上這事≌偻ⅲ” “怎么了凳厢?”我有些...
    開(kāi)封第一講書(shū)人閱讀 156,852評(píng)論 0 347
  • 文/不壞的土叔 我叫張陵账胧,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我先紫,道長(zhǎng)治泥,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,408評(píng)論 1 283
  • 正文 為了忘掉前任遮精,我火速辦了婚禮居夹,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘本冲。我一直安慰自己准脂,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,445評(píng)論 5 384
  • 文/花漫 我一把揭開(kāi)白布檬洞。 她就那樣靜靜地躺著狸膏,像睡著了一般。 火紅的嫁衣襯著肌膚如雪添怔。 梳的紋絲不亂的頭發(fā)上湾戳,一...
    開(kāi)封第一講書(shū)人閱讀 49,772評(píng)論 1 290
  • 那天,我揣著相機(jī)與錄音广料,去河邊找鬼砾脑。 笑死,一個(gè)胖子當(dāng)著我的面吹牛艾杏,可吹牛的內(nèi)容都是我干的韧衣。 我是一名探鬼主播,決...
    沈念sama閱讀 38,921評(píng)論 3 406
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼糜颠,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼汹族!你這毒婦竟也來(lái)了萧求?” 一聲冷哼從身側(cè)響起其兴,我...
    開(kāi)封第一講書(shū)人閱讀 37,688評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎夸政,沒(méi)想到半個(gè)月后元旬,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,130評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡守问,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,467評(píng)論 2 325
  • 正文 我和宋清朗相戀三年匀归,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片耗帕。...
    茶點(diǎn)故事閱讀 38,617評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡穆端,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出仿便,到底是詐尸還是另有隱情体啰,我是刑警寧澤攒巍,帶...
    沈念sama閱讀 34,276評(píng)論 4 329
  • 正文 年R本政府宣布,位于F島的核電站荒勇,受9級(jí)特大地震影響柒莉,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜沽翔,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,882評(píng)論 3 312
  • 文/蒙蒙 一兢孝、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧仅偎,春花似錦跨蟹、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,740評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至威恼,卻和暖如春品姓,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背箫措。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,967評(píng)論 1 265
  • 我被黑心中介騙來(lái)泰國(guó)打工腹备, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人斤蔓。 一個(gè)月前我還...
    沈念sama閱讀 46,315評(píng)論 2 360
  • 正文 我出身青樓植酥,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親弦牡。 傳聞我的和親對(duì)象是個(gè)殘疾皇子友驮,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,486評(píng)論 2 348

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