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

volatile_logo

我們知道有時候一個對象的共享變量會被多個線程所訪問,這時就會有線程安全問題。當然我們可以使用synchorinized 關鍵字來為此變量加鎖,進行同步處理。從而限制只能有一個線程來使用此變量,但是加鎖會大大影響程序執行效率,此外我們還可以使用ThreadLocal來解決對某一個變量的訪問沖突問題。

一、ThreadLocal 概述

當使用ThreadLocal維護變量的時候 它為每一個使用該變量的線程提供一個獨立的變量副本,即每個線程內部都會有一個該變量,這樣同時多個線程訪問該變量并不會彼此相互影響,因此他們使用的都是自己從內存中拷貝過來的變量的副本, 這樣就不存在線程安全問題,也不會影響程序的執行性能。
ThreadLocal 的幾個方法: ThreadLocal 可以存儲任何類型的變量對象, get返回的是一個Object對象,但是我們可以通過泛型來制定存儲對象的類型。

public T get() { } // 用來獲取ThreadLocal在當前線程中保存的變量副本
public void set(T value) { } //set()用來設置當前線程中變量的副本
public void remove() { } //remove()用來移除當前線程中變量的副本
protected T initialValue() { } //initialValue()是一個protected方法,一般是用來在使用時進行重寫的

Thread 在內部是通過ThreadLocalMap來維護ThreadLocal變量表, 在Thread類中有一個threadLocals 變量,是ThreadLocalMap類型的,它就是為每一個線程來存儲自身的ThreadLocal變量的, ThreadLocalMap是ThreadLocal類的一個內部類,這個Map里面的最小的存儲單位是一個Entry, 它使用ThreadLocal作為key, 變量作為 value,這是因為在每一個線程里面,可能存在著多個ThreadLocal變量

初始時,在Thread里面,threadLocals為空,當通過ThreadLocal變量調用get()方法或者set()方法,就會對Thread類中的threadLocals進行初始化,并且以當前ThreadLocal變量為鍵值,以ThreadLocal要保存的副本變量為value,存到threadLocals。
然后在當前線程里面,如果要使用副本變量,就可以通過get方法在threadLocals里面查找
我們來看一個使用示例:

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

輸出結果為

1 
main 
9 
Thread-0 
1 
main

二、父子線程傳遞InheritableThreadLocal

以上方案在父子線程中就有了局限性,如果子線程想要拿到父線程的中的ThreadLocal值怎么辦呢?比如會有以下的這種代碼的實現。由于ThreadLocal的實現機制,在子線程中get時,我們拿到的Thread對象是當前子線程對象,那么他的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已經為這種情況提供了實現方案:InheritableThreadLocal。大致的解釋了一下InheritableThreadLocal為什么能解決父子線程傳遞Threadlcoal值的問題。
1)在創建InheritableThreadLocal對象的時候賦值給線程的t.inheritableThreadLocals變量
2)在創建新線程的時候會check父線程中t.inheritableThreadLocals變量是否為null,如果不為null則copy一份ThradLocalMap到子線程的t.inheritableThreadLocals成員變量中去
3)因為復寫了getMap(Thread)和CreateMap()方法,所以get值得時候,就可以在getMap(t)的時候就會從t.inheritableThreadLocals中拿到map對象,從而實現了可以拿到父線程ThreadLocal中的值

所以,在最開始的代碼示例中,如果把ThreadLocal對象換成InheritableThreadLocal對象,那么get到的字符會是“xiezhaodong”而不是NULL

二、線程池傳遞TransmittableThreadLocal

我們在使用線程的時候往往不會只是簡單的new Thrad對象,而是使用線程池,當然線程池的好處多多。這里不詳解,既然這里提出了問題,那么線程池會給InheritableThreadLocal帶來什么問題呢?我們列舉一下線程池的特點:

1)為了減小創建線程的開銷,線程池會緩存已經使用過的線程
2)生命周期統一管理,合理的分配系統資源

對于第一點,如果一個子線程已經使用過,并且會set新的值到ThreadLocal中,那么第二個task提交進來的時候還能獲得父線程中的值嗎?答案是不能,如果我們能夠,在使用完這個線程的時候清除所有的localMap,在submit新任務的時候在重新重父線程中copy所有的Entry。然后重新給當前線程的t.inhertableThreadLocal賦值。這樣就能夠解決在線程池中每一個新的任務都能夠獲得父線程中ThreadLocal中的值而不受其他任務的影響,因為在生命周期完成的時候會自動clear所有的數據。Alibaba的一個庫解決了這個問題github:alibaba/transmittable-thread-local
如何使用
這個庫最簡單的方式是這樣使用的,通過簡單的修飾,使得提交的runable擁有了上一節所述的功能。具體的API文檔詳見github,這里不再贅述

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

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

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

原理簡述
這個方法TtlRunnable.get(task)最終會調用構造方法,返回的是該類本身,也是一個Runable,這樣就完成了簡單的裝飾。最重要的是在run方法這個地方。

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值到本類當中
        this.copiedRef = new AtomicReference<Map<TransmittableThreadLocal<?>, Object>>(TransmittableThreadLocal.copy());
        this.runnable = runnable;//提交的runable,被修飾對象
        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!");
        }
        //裝載到當前線程
        Map<TransmittableThreadLocal<?>, Object> backup = TransmittableThreadLocal.backupAndSetToCopied(copied);
        try {
            runnable.run();//執行提交的task
        } finally {
        //clear
            TransmittableThreadLocal.restoreBackup(backup);
        }
    }
}

在上面的使用線程池的例子當中,如果換成這種修飾的方式進行操作,B任務得到的肯定是父線程中ThreadLocal的值,解決了在線程池中InheritableThreadLocal不能解決的問題。
如何更新父線程ThreadLocal值?
如果線程之間出了要能夠得到父線程中的值,同時想更新值怎么辦呢?在前面我們有提到,當子線程copy父線程的ThreadLocalMap的時候是淺拷貝的,代表子線程Entry里面的value都是指向的同一個引用,我們只要修改這個引用的同時就能夠修改父線程當中的值了,比如這樣:

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

這樣父線程中的值就會得到更新了。能夠滿足父線程ThreadLocal值的實時更新,同時子線程也能共享父線程的值。不過場景倒是不是很常見的樣子。

參考文章


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

?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,224評論 6 529
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 97,916評論 3 413
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,014評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,466評論 1 308
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,245評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,795評論 1 320
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,869評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,010評論 0 285
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,524評論 1 331
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,487評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,634評論 1 366
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,173評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,884評論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,282評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,541評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,236評論 3 388
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,623評論 2 370

推薦閱讀更多精彩內容