JAVA并發(fā)編程(六):線程本地變量ThreadLocal與TransmittableThreadLocal

volatile_logo

我們知道有時(shí)候一個(gè)對(duì)象的共享變量會(huì)被多個(gè)線程所訪問(wèn)涯呻,這時(shí)就會(huì)有線程安全問(wèn)題。當(dāng)然我們可以使用synchorinized 關(guān)鍵字來(lái)為此變量加鎖叉趣,進(jìn)行同步處理胜臊。從而限制只能有一個(gè)線程來(lái)使用此變量,但是加鎖會(huì)大大影響程序執(zhí)行效率裙犹,此外我們還可以使用ThreadLocal來(lái)解決對(duì)某一個(gè)變量的訪問(wèn)沖突問(wèn)題尽狠。

一、ThreadLocal 概述

當(dāng)使用ThreadLocal維護(hù)變量的時(shí)候 它為每一個(gè)使用該變量的線程提供一個(gè)獨(dú)立的變量副本叶圃,即每個(gè)線程內(nèi)部都會(huì)有一個(gè)該變量晚唇,這樣同時(shí)多個(gè)線程訪問(wèn)該變量并不會(huì)彼此相互影響,因此他們使用的都是自己從內(nèi)存中拷貝過(guò)來(lái)的變量的副本盗似, 這樣就不存在線程安全問(wèn)題哩陕,也不會(huì)影響程序的執(zhí)行性能。
ThreadLocal 的幾個(gè)方法: ThreadLocal 可以存儲(chǔ)任何類型的變量對(duì)象赫舒, get返回的是一個(gè)Object對(duì)象悍及,但是我們可以通過(guò)泛型來(lái)制定存儲(chǔ)對(duì)象的類型。

public T get() { } // 用來(lái)獲取ThreadLocal在當(dāng)前線程中保存的變量副本
public void set(T value) { } //set()用來(lái)設(shè)置當(dāng)前線程中變量的副本
public void remove() { } //remove()用來(lái)移除當(dāng)前線程中變量的副本
protected T initialValue() { } //initialValue()是一個(gè)protected方法接癌,一般是用來(lái)在使用時(shí)進(jìn)行重寫(xiě)的

Thread 在內(nèi)部是通過(guò)ThreadLocalMap來(lái)維護(hù)ThreadLocal變量表心赶, 在Thread類中有一個(gè)threadLocals 變量,是ThreadLocalMap類型的缺猛,它就是為每一個(gè)線程來(lái)存儲(chǔ)自身的ThreadLocal變量的缨叫, ThreadLocalMap是ThreadLocal類的一個(gè)內(nèi)部類,這個(gè)Map里面的最小的存儲(chǔ)單位是一個(gè)Entry荔燎, 它使用ThreadLocal作為key耻姥, 變量作為 value,這是因?yàn)樵诿恳粋€(gè)線程里面有咨,可能存在著多個(gè)ThreadLocal變量

初始時(shí)琐簇,在Thread里面,threadLocals為空座享,當(dāng)通過(guò)ThreadLocal變量調(diào)用get()方法或者set()方法婉商,就會(huì)對(duì)Thread類中的threadLocals進(jìn)行初始化似忧,并且以當(dāng)前ThreadLocal變量為鍵值,以ThreadLocal要保存的副本變量為value丈秩,存到threadLocals盯捌。
然后在當(dāng)前線程里面,如果要使用副本變量蘑秽,就可以通過(guò)get方法在threadLocals里面查找
我們來(lái)看一個(gè)使用示例:

public class Test {
    ThreadLocal<Long> longLocal = new ThreadLocal<Long>();
    ThreadLocal<String> stringLocal = new ThreadLocal<String>();
 
     
    public void set() {
        longLocal.set(Thread.currentThread().getId());
        stringLocal.set(Thread.currentThread().getName());
    }
     
    public long getLong() {
        return longLocal.get();
    }
     
    public String getString() {
        return stringLocal.get();
    }
     
    public static void main(String[] args) throws InterruptedException {
        final Test test = new Test();
         
         
        test.set();
        System.out.println(test.getLong());
        System.out.println(test.getString());
     
         
        Thread thread1 = new Thread(){
            public void run() {
                test.set();
                System.out.println(test.getLong());
                System.out.println(test.getString());
            };
        };
        thread1.start();
        thread1.join();
         
        System.out.println(test.getLong());
        System.out.println(test.getString());
    }
}

輸出結(jié)果為

1 
main 
9 
Thread-0 
1 
main

二挽唉、父子線程傳遞InheritableThreadLocal

以上方案在父子線程中就有了局限性,如果子線程想要拿到父線程的中的ThreadLocal值怎么辦呢筷狼?比如會(huì)有以下的這種代碼的實(shí)現(xiàn)。由于ThreadLocal的實(shí)現(xiàn)機(jī)制,在子線程中g(shù)et時(shí),我們拿到的Thread對(duì)象是當(dāng)前子線程對(duì)象,那么他的ThreadLocalMap是null的,所以我們得到的value也是null匠童。

final ThreadLocal threadLocal=new ThreadLocal(){
            @Override
            protected Object initialValue() {
                return "xiezhaodong";
            }
        };
 new Thread(new Runnable() {
            @Override
            public void run() {
                threadLocal.get();//NULL
            }
        }).start();

JDK已經(jīng)為這種情況提供了實(shí)現(xiàn)方案:InheritableThreadLocal埂材。大致的解釋了一下InheritableThreadLocal為什么能解決父子線程傳遞Threadlcoal值的問(wèn)題。
1)在創(chuàng)建InheritableThreadLocal對(duì)象的時(shí)候賦值給線程的t.inheritableThreadLocals變量
2)在創(chuàng)建新線程的時(shí)候會(huì)check父線程中t.inheritableThreadLocals變量是否為null汤求,如果不為null則copy一份ThradLocalMap到子線程的t.inheritableThreadLocals成員變量中去
3)因?yàn)閺?fù)寫(xiě)了getMap(Thread)和CreateMap()方法,所以get值得時(shí)候俏险,就可以在getMap(t)的時(shí)候就會(huì)從t.inheritableThreadLocals中拿到map對(duì)象,從而實(shí)現(xiàn)了可以拿到父線程ThreadLocal中的值

所以,在最開(kāi)始的代碼示例中扬绪,如果把ThreadLocal對(duì)象換成InheritableThreadLocal對(duì)象竖独,那么get到的字符會(huì)是“xiezhaodong”而不是NULL

二、線程池傳遞TransmittableThreadLocal

我們?cè)谑褂镁€程的時(shí)候往往不會(huì)只是簡(jiǎn)單的new Thrad對(duì)象挤牛,而是使用線程池莹痢,當(dāng)然線程池的好處多多。這里不詳解墓赴,既然這里提出了問(wèn)題竞膳,那么線程池會(huì)給InheritableThreadLocal帶來(lái)什么問(wèn)題呢?我們列舉一下線程池的特點(diǎn):

1)為了減小創(chuàng)建線程的開(kāi)銷诫硕,線程池會(huì)緩存已經(jīng)使用過(guò)的線程
2)生命周期統(tǒng)一管理,合理的分配系統(tǒng)資源

對(duì)于第一點(diǎn)坦辟,如果一個(gè)子線程已經(jīng)使用過(guò),并且會(huì)set新的值到ThreadLocal中章办,那么第二個(gè)task提交進(jìn)來(lái)的時(shí)候還能獲得父線程中的值嗎锉走?答案是不能,如果我們能夠藕届,在使用完這個(gè)線程的時(shí)候清除所有的localMap挪蹭,在submit新任務(wù)的時(shí)候在重新重父線程中copy所有的Entry。然后重新給當(dāng)前線程的t.inhertableThreadLocal賦值休偶。這樣就能夠解決在線程池中每一個(gè)新的任務(wù)都能夠獲得父線程中ThreadLocal中的值而不受其他任務(wù)的影響嚣潜,因?yàn)樵谏芷谕瓿傻臅r(shí)候會(huì)自動(dòng)clear所有的數(shù)據(jù)。Alibaba的一個(gè)庫(kù)解決了這個(gè)問(wèn)題github:alibaba/transmittable-thread-local
如何使用
這個(gè)庫(kù)最簡(jiǎn)單的方式是這樣使用的,通過(guò)簡(jiǎn)單的修飾椅贱,使得提交的runable擁有了上一節(jié)所述的功能懂算。具體的API文檔詳見(jiàn)github只冻,這里不再贅述

TransmittableThreadLocal<String> parent = new TransmittableThreadLocal<String>();
parent.set("value-set-in-parent");

Runnable task = new Task("1");
// 額外的處理,生成修飾了的對(duì)象ttlRunnable
Runnable ttlRunnable = TtlRunnable.get(task); 
executorService.submit(ttlRunnable);

// Task中可以讀取, 值是"value-set-in-parent"
String value = parent.get();

原理簡(jiǎn)述
這個(gè)方法TtlRunnable.get(task)最終會(huì)調(diào)用構(gòu)造方法计技,返回的是該類本身喜德,也是一個(gè)Runable,這樣就完成了簡(jiǎn)單的裝飾。最重要的是在run方法這個(gè)地方垮媒。

public final class TtlRunnable implements Runnable {
    private final AtomicReference<Map<TransmittableThreadLocal<?>, Object>> copiedRef;
    private final Runnable runnable;
    private final boolean releaseTtlValueReferenceAfterRun;

    private TtlRunnable(Runnable runnable, boolean releaseTtlValueReferenceAfterRun) {
    //從父類copy值到本類當(dāng)中
        this.copiedRef = new AtomicReference<Map<TransmittableThreadLocal<?>, Object>>(TransmittableThreadLocal.copy());
        this.runnable = runnable;//提交的runable,被修飾對(duì)象
        this.releaseTtlValueReferenceAfterRun = releaseTtlValueReferenceAfterRun;
    }
    /**
     * wrap method {@link Runnable#run()}.
     */
    @Override
    public void run() {
        Map<TransmittableThreadLocal<?>, Object> copied = copiedRef.get();
        if (copied == null || releaseTtlValueReferenceAfterRun && !copiedRef.compareAndSet(copied, null)) {
            throw new IllegalStateException("TTL value reference is released after run!");
        }
        //裝載到當(dāng)前線程
        Map<TransmittableThreadLocal<?>, Object> backup = TransmittableThreadLocal.backupAndSetToCopied(copied);
        try {
            runnable.run();//執(zhí)行提交的task
        } finally {
        //clear
            TransmittableThreadLocal.restoreBackup(backup);
        }
    }
}

在上面的使用線程池的例子當(dāng)中舍悯,如果換成這種修飾的方式進(jìn)行操作,B任務(wù)得到的肯定是父線程中ThreadLocal的值睡雇,解決了在線程池中InheritableThreadLocal不能解決的問(wèn)題萌衬。
如何更新父線程ThreadLocal值?
如果線程之間出了要能夠得到父線程中的值它抱,同時(shí)想更新值怎么辦呢秕豫?在前面我們有提到,當(dāng)子線程copy父線程的ThreadLocalMap的時(shí)候是淺拷貝的,代表子線程Entry里面的value都是指向的同一個(gè)引用观蓄,我們只要修改這個(gè)引用的同時(shí)就能夠修改父線程當(dāng)中的值了,比如這樣:

@Override
          public void run() {
              System.out.println("========");
              Span span=  inheritableThreadLocal.get();
              System.out.println(span);
              span.name="liuliuliu";//修改父引用為liuliuliu
              inheritableThreadLocal.set(new Span("zhangzhangzhang"));
              System.out.println(inheritableThreadLocal.get());
          }

這樣父線程中的值就會(huì)得到更新了混移。能夠滿足父線程ThreadLocal值的實(shí)時(shí)更新,同時(shí)子線程也能共享父線程的值侮穿。不過(guò)場(chǎng)景倒是不是很常見(jiàn)的樣子歌径。

參考文章


本文作者: catalinaLi
本文鏈接: http://catalinali.top/2018/helloThreadLocal/

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市亲茅,隨后出現(xiàn)的幾起案子回铛,更是在濱河造成了極大的恐慌,老刑警劉巖克锣,帶你破解...
    沈念sama閱讀 217,277評(píng)論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件勺届,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡娶耍,警方通過(guò)查閱死者的電腦和手機(jī)免姿,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評(píng)論 3 393
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)榕酒,“玉大人胚膊,你說(shuō)我怎么就攤上這事∠胗ィ” “怎么了紊婉?”我有些...
    開(kāi)封第一講書(shū)人閱讀 163,624評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)辑舷。 經(jīng)常有香客問(wèn)我喻犁,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,356評(píng)論 1 293
  • 正文 為了忘掉前任肢础,我火速辦了婚禮还栓,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘传轰。我一直安慰自己剩盒,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,402評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布慨蛙。 她就那樣靜靜地躺著辽聊,像睡著了一般。 火紅的嫁衣襯著肌膚如雪期贫。 梳的紋絲不亂的頭發(fā)上跟匆,一...
    開(kāi)封第一講書(shū)人閱讀 51,292評(píng)論 1 301
  • 那天,我揣著相機(jī)與錄音通砍,去河邊找鬼玛臂。 笑死,一個(gè)胖子當(dāng)著我的面吹牛埠帕,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播玖绿,決...
    沈念sama閱讀 40,135評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼敛瓷,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了斑匪?” 一聲冷哼從身側(cè)響起呐籽,我...
    開(kāi)封第一講書(shū)人閱讀 38,992評(píng)論 0 275
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎蚀瘸,沒(méi)想到半個(gè)月后狡蝶,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,429評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡贮勃,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,636評(píng)論 3 334
  • 正文 我和宋清朗相戀三年贪惹,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片寂嘉。...
    茶點(diǎn)故事閱讀 39,785評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡奏瞬,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出泉孩,到底是詐尸還是另有隱情硼端,我是刑警寧澤,帶...
    沈念sama閱讀 35,492評(píng)論 5 345
  • 正文 年R本政府宣布寓搬,位于F島的核電站珍昨,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜镣典,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,092評(píng)論 3 328
  • 文/蒙蒙 一兔毙、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧骆撇,春花似錦瞒御、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,723評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至涌乳,卻和暖如春蜻懦,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背夕晓。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,858評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工宛乃, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人蒸辆。 一個(gè)月前我還...
    沈念sama閱讀 47,891評(píng)論 2 370
  • 正文 我出身青樓征炼,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親躬贡。 傳聞我的和親對(duì)象是個(gè)殘疾皇子谆奥,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,713評(píng)論 2 354

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