ThreadLocal及其擴(kuò)展

ThreadLocal

ThreadLocal是線程本地變量蛉签,每個(gè)線程往這個(gè)ThreadLocal中讀寫是線程隔離,互相之間不會(huì)影響的沥寥。它提供了一種將可變數(shù)據(jù)通過(guò)每個(gè)線程有自己的獨(dú)立副本從而實(shí)現(xiàn)線程封閉的機(jī)制碍舍。
Thread類有一個(gè)類型為ThreadLocal.ThreadLocalMap的實(shí)例變量threadLocals,也就是說(shuō)每個(gè)線程有一個(gè)自己的ThreadLocalMap邑雅。ThreadLocalMap參照HashMap的實(shí)現(xiàn)片橡,ThreadLocalMap的key為ThreadLocal,value為代碼中放入的值(實(shí)際上key并不是ThreadLocal本身淮野,而是它的一個(gè)弱引用)捧书。每個(gè)線程在往某個(gè)ThreadLocal里塞值的時(shí)候,都會(huì)往自己的ThreadLocalMap里存骤星,讀也是以某個(gè)ThreadLocal作為引用经瓷,在自己的map里找對(duì)應(yīng)的key,從而實(shí)現(xiàn)了線程隔離洞难。

ThreadLocalMap

ThreadLocalMap是ThreadLocal的靜態(tài)內(nèi)部類舆吮。ThreadLocalMap提供了一種為ThreadLocal定制的高效實(shí)現(xiàn),并且自帶一種基于弱引用的垃圾清理機(jī)制。
ThreadLocalMap是類似HashMap實(shí)現(xiàn)的另一種map實(shí)現(xiàn)歪泳,有自己的key和value萝勤,key為ThreadLocal,value為代碼中放入的值。和HashMap一樣有一個(gè)table數(shù)據(jù),有g(shù)et和set方法俐填,在key的hash沖突時(shí)往下尋找對(duì)應(yīng)對(duì)象槽位。
ThreadLocalMap里的節(jié)點(diǎn)Entry不同于HashMap里面的Entry趟径,繼承弱引用,是如下定義的癣防。

static class Entry extends WeakReference<java.lang.ThreadLocal<?>> {
   // 往ThreadLocal里實(shí)際塞入的值
   Object value;
   Entry(java.lang.ThreadLocal<?> k, Object v) {
       super(k);
       value = v;
   }
}

為什么用弱引用
弱引用是Java中四檔引用的第三檔蜗巧,比軟引用更加弱一些,如果一個(gè)對(duì)象沒(méi)有強(qiáng)引用鏈可達(dá)蕾盯,那么一般活不過(guò)下一次GC幕屹。當(dāng)某個(gè)ThreadLocal已經(jīng)沒(méi)有強(qiáng)引用可達(dá),則隨著它被垃圾回收级遭,在ThreadLocalMap里對(duì)應(yīng)的Entry的鍵值會(huì)失效望拖,這為ThreadLocalMap本身的垃圾清理提供了便利。
ThreadLocal的get和set方法

  public T get() {
        Thread t = Thread.currentThread(); //獲取當(dāng)前線程
        ThreadLocalMap map = getMap(t); // 獲取當(dāng)前線程對(duì)象的實(shí)例變量threadLocals
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }

set方法邏輯同get方法

public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

InheritableThreadLocal

InheritableThreadLocal繼承ThreadLocal挫鸽,ThreadLocal在線程內(nèi)實(shí)現(xiàn)一個(gè)局部變量说敏,可以在線程的任何地方來(lái)訪問(wèn),能夠減少參數(shù)的傳遞丢郊,InheritableThreadLocal在子線程和父線程之間共享線程實(shí)例本地變量盔沫,也同樣是為了減少參數(shù)的傳遞。
ThreadLocal使用的是Thread的實(shí)例變量threadLocals枫匾;InheritableThreadLocal使用的是Thread的實(shí)例變量inheritableThreadLocals架诞。這兩個(gè)變量類型都為ThreadLocal.ThreadLocalMap,用途不一樣干茉。
InheritableThreadLocal的實(shí)現(xiàn)如下:

public class InheritableThreadLocal<T> extends ThreadLocal<T> {
  
    protected T childValue(T parentValue) {
        return parentValue;
    }

    ThreadLocalMap getMap(Thread t) {
       return t.inheritableThreadLocals;
    }

    void createMap(Thread t, T firstValue) {
        t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
    }
}

InheritableThreadLocal重載了getMap和createMap方法侈贷,操作的不再是threadLocals,而是inheritableThreadLocals等脂。
在Thread初始化時(shí)有以下邏輯。

if (parent.inheritableThreadLocals != null)
            this.inheritableThreadLocals =
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);

這個(gè)保證了inheritableThreadLocals變量從父Thread傳遞到子Thread撑蚌。

Transmittable ThreadLocal(TTL)

ThreadLocal實(shí)現(xiàn)了線程本地變量隔離,每個(gè)線程可以擁有不會(huì)被別的線程干擾的線程值map上遥。
InheritableThreadLocal繼承ThreadLocal,InheritableThreadLocal可以在子線程和父線程之間共享線程實(shí)例本地變量争涌。
JDK的InheritableThreadLocal類可以完成父線程到子線程的值傳遞粉楚。但對(duì)于使用線程池等會(huì)池化復(fù)用線程的組件的情況,線程由線程池創(chuàng)建好,并且線程是池化起來(lái)反復(fù)使用的模软;這時(shí)父子線程關(guān)系的ThreadLocal值傳遞已經(jīng)沒(méi)有意義伟骨,應(yīng)用需要的實(shí)際上是把 任務(wù)提交給線程池時(shí)的線程的ThreadLocal值傳遞到 任務(wù)執(zhí)行時(shí)的Thread。
這種場(chǎng)景需要TransmittableThreadLocal類繼承并加強(qiáng)InheritableThreadLocal類燃异,解決上述的問(wèn)題携狭。
框架/中間件集成TTL(TransmittableThreadLocal)傳遞,通過(guò)TransmittableThreadLocal.Transmitter 抓取當(dāng)前線程的所有TTL值并在其他線程進(jìn)行回放回俐;在回放線程執(zhí)行完業(yè)務(wù)操作后逛腿,恢復(fù)為回放線程原來(lái)的TTL值。

TransmittableThreadLocal.Transmitter提供了所有TTL值的抓取仅颇、回放和恢復(fù)方法(即CRR操作):

  1. capture方法:抓取線程(線程A)的所有TTL值单默。
  2. replay方法:在另一個(gè)線程(線程B)中,回放在capture方法中抓取的TTL值(回放的過(guò)程也就是把第一步抓取的線程實(shí)例本地變量設(shè)置到當(dāng)前線程的實(shí)例本地變量中)忘瓦,并返回 回放前TTL值的備份
  3. restore方法:恢復(fù)線程B執(zhí)行replay方法之前的TTL值(即備份搁廓,把第二步返回的回放前TTL值重新設(shè)置回池化中線程的實(shí)例本地變量)

整個(gè)過(guò)程的完整時(shí)序圖

image.png

原理簡(jiǎn)析
對(duì)Runnable和Callable進(jìn)行裝飾,形成TtlRunnable和TtlCallable耕皮,在這兩個(gè)裝飾類里面主要多了一個(gè)引用對(duì)象(存放線程切換時(shí)線程的本地變量引用)境蜕。

private final AtomicReference<Object> capturedRef; //用戶存放線程本地變量的map引用

再來(lái)看一下CRR操作的核心源碼,CRR的代碼邏輯時(shí)在TransmittableThreadLocal的靜態(tài)內(nèi)部類Transmitter中:
capture

public static Object capture() {
            Map<TransmittableThreadLocal<?>, Object> captured = new HashMap<TransmittableThreadLocal<?>, Object>();
            //holder是TransmittableThreadLocal類的靜態(tài)對(duì)象,類型為InheritableThreadLocal<Map<TransmittableThreadLocal<?>, ?>>明场,用于存儲(chǔ)線程的本地變量汽摹,這一份本地變量被TransmittableThreadLocal用于CRR操作
            for (TransmittableThreadLocal<?> threadLocal : holder.get().keySet()) {
                captured.put(threadLocal, threadLocal.copyValue());
            }
            return captured;
        }

repaly

public static Object replay(Object captured) {
            @SuppressWarnings("unchecked")
            Map<TransmittableThreadLocal<?>, Object> capturedMap = (Map<TransmittableThreadLocal<?>, Object>) captured;
            Map<TransmittableThreadLocal<?>, Object> backup = new HashMap<TransmittableThreadLocal<?>, Object>();

            for (Iterator<? extends Map.Entry<TransmittableThreadLocal<?>, ?>> iterator = holder.get().entrySet().iterator();
                 iterator.hasNext(); ) {
                Map.Entry<TransmittableThreadLocal<?>, ?> next = iterator.next();
                TransmittableThreadLocal<?> threadLocal = next.getKey();
                // backup
                backup.put(threadLocal, threadLocal.get());
                // clear the TTL value only in captured
                if (!capturedMap.containsKey(threadLocal)) {
                    iterator.remove();
                    threadLocal.superRemove();
                }
            }
            // 把capture抓取的線程本地變量設(shè)置到當(dāng)前線程的本地變量中
            for (Map.Entry<TransmittableThreadLocal<?>, Object> entry : capturedMap.entrySet()) {
                @SuppressWarnings("unchecked")
                TransmittableThreadLocal<Object> threadLocal = (TransmittableThreadLocal<Object>) entry.getKey();
                threadLocal.set(entry.getValue());
            }
            // call beforeExecute callback
            doExecuteCallback(true);
            return backup;
        }

restore

public static void restore(Object backup) {
            @SuppressWarnings("unchecked")
            Map<TransmittableThreadLocal<?>, Object> backupMap = (Map<TransmittableThreadLocal<?>, Object>) backup;
            // call afterExecute callback
            doExecuteCallback(false);

            for (Iterator<? extends Map.Entry<TransmittableThreadLocal<?>, ?>> iterator = holder.get().entrySet().iterator();
                 iterator.hasNext(); ) {
                Map.Entry<TransmittableThreadLocal<?>, ?> next = iterator.next();
                TransmittableThreadLocal<?> threadLocal = next.getKey();

                // clear the TTL value only in backup
                // avoid the extra value of backup after restore
                if (!backupMap.containsKey(threadLocal)) {
                    iterator.remove();
                    threadLocal.superRemove();
                }
            }

            // 把replay步驟備份前`TTL`值重新設(shè)置回池化中線程的本地變量
            for (Map.Entry<TransmittableThreadLocal<?>, Object> entry : backupMap.entrySet()) {
                @SuppressWarnings("unchecked")
                TransmittableThreadLocal<Object> threadLocal = (TransmittableThreadLocal<Object>) entry.getKey();
                threadLocal.set(entry.getValue());
            }
        }

業(yè)務(wù)無(wú)感知
如果想讓TTL框架更通用,對(duì)已經(jīng)的業(yè)務(wù)場(chǎng)景進(jìn)行無(wú)縫支持苦锨。需要把對(duì)Runnable逼泣、Callable、ExecuteService等池化類的裝飾操作借助字節(jié)碼在JVM啟動(dòng)時(shí)自動(dòng)完成舟舒。
使用Java Agent來(lái)修飾JDK線程池實(shí)現(xiàn)類拉庶。這種方式實(shí)現(xiàn)線程池的傳遞是透明的,代碼中沒(méi)有修飾Runnable或是線程池的代碼秃励。即可以做到應(yīng)用代碼 無(wú)侵入氏仗。
關(guān)于 無(wú)侵入 的更多說(shuō)明參見文檔Java Agent方式對(duì)應(yīng)用代碼無(wú)侵入。后面會(huì)寫專門的文章介紹字節(jié)碼增強(qiáng)這塊的技術(shù)夺鲜,這里就不展開了皆尔。

參考資料

JDK源碼
https://github.com/alibaba/transmittable-thread-local#21-%E4%BF%AE%E9%A5%B0runnable%E5%92%8Ccallable

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市币励,隨后出現(xiàn)的幾起案子慷蠕,更是在濱河造成了極大的恐慌,老刑警劉巖食呻,帶你破解...
    沈念sama閱讀 211,123評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件流炕,死亡現(xiàn)場(chǎng)離奇詭異澎现,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)每辟,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,031評(píng)論 2 384
  • 文/潘曉璐 我一進(jìn)店門剑辫,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人渠欺,你說(shuō)我怎么就攤上這事妹蔽。” “怎么了峻堰?”我有些...
    開封第一講書人閱讀 156,723評(píng)論 0 345
  • 文/不壞的土叔 我叫張陵讹开,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我捐名,道長(zhǎng)旦万,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,357評(píng)論 1 283
  • 正文 為了忘掉前任镶蹋,我火速辦了婚禮成艘,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘贺归。我一直安慰自己淆两,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,412評(píng)論 5 384
  • 文/花漫 我一把揭開白布拂酣。 她就那樣靜靜地躺著秋冰,像睡著了一般。 火紅的嫁衣襯著肌膚如雪婶熬。 梳的紋絲不亂的頭發(fā)上剑勾,一...
    開封第一講書人閱讀 49,760評(píng)論 1 289
  • 那天,我揣著相機(jī)與錄音赵颅,去河邊找鬼虽另。 笑死,一個(gè)胖子當(dāng)著我的面吹牛饺谬,可吹牛的內(nèi)容都是我干的捂刺。 我是一名探鬼主播,決...
    沈念sama閱讀 38,904評(píng)論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼募寨,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼族展!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起拔鹰,我...
    開封第一講書人閱讀 37,672評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤仪缸,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后格郁,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,118評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,456評(píng)論 2 325
  • 正文 我和宋清朗相戀三年例书,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了锣尉。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,599評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡决采,死狀恐怖自沧,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情树瞭,我是刑警寧澤拇厢,帶...
    沈念sama閱讀 34,264評(píng)論 4 328
  • 正文 年R本政府宣布,位于F島的核電站晒喷,受9級(jí)特大地震影響孝偎,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜凉敲,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,857評(píng)論 3 312
  • 文/蒙蒙 一衣盾、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧爷抓,春花似錦势决、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,731評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至渤昌,卻和暖如春虽抄,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背耘沼。 一陣腳步聲響...
    開封第一講書人閱讀 31,956評(píng)論 1 264
  • 我被黑心中介騙來(lái)泰國(guó)打工极颓, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人群嗤。 一個(gè)月前我還...
    沈念sama閱讀 46,286評(píng)論 2 360
  • 正文 我出身青樓菠隆,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親狂秘。 傳聞我的和親對(duì)象是個(gè)殘疾皇子骇径,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,465評(píng)論 2 348

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