線程池如何傳遞ThreadLocal

file

前言

在做分布式鏈路追蹤系統(tǒng)的時候要糊,需要解決異步調(diào)用透傳上下文的需求,特別是傳遞traceId,本文就線程池透傳幾種方式進(jìn)行分析货岭。

其他典型場景例子:

  1. 分布式跟蹤系統(tǒng) 或 全鏈路壓測(即鏈路打標(biāo))

  2. 日志收集記錄系統(tǒng)上下文

  3. SessionCache

  4. 應(yīng)用容器或上層框架跨應(yīng)用代碼給下層SDK傳遞信息

1、JDK對跨線程傳遞ThreadLocal的支持

首先看一個最簡單場景疾渴,也是一個錯誤的例子千贯。

    void testThreadLocal(){
        ThreadLocal<Object> threadLocal = new ThreadLocal<>();
        threadLocal.set("not ok");
        new Thread(()->{
            System.out.println(threadLocal.get());
        }).start();
    }

java中的threadlocal,是綁定在線程上的搞坝。你在一個線程中set的值搔谴,在另外一個線程是拿不到的。

上面的輸出是:

null

1.1 InheritableThreadLocal 例子

JDK考慮了這種場景桩撮,實現(xiàn)了InheritableThreadLocal ,不要高興太早,這個只是支持父子線程敦第,線程池會有問題峰弹。

我們看下InheritableThreadLocal的例子:

        InheritableThreadLocal<String> itl = new InheritableThreadLocal<>();
        itl.set("father");
        new Thread(()->{
            System.out.println("subThread:" + itl.get());
            itl.set("son");
            System.out.println(itl.get());
        }).start();

        Thread.sleep(500);//等待子線程執(zhí)行完

        System.out.println("thread:" + itl.get());

上面的輸出是:

subThread:father //子線程可以拿到父線程的變量

son

thread:father //子線程修改不影響父線程的變量

1.2 InheritableThreadLocal的實現(xiàn)原理

有同學(xué)可能想知道InheritableThreadLocal的實現(xiàn)原理,其實特別簡單芜果。就是Thread類里面分開記錄了ThreadLocal鞠呈、InheritableThreadLocal的ThreadLocalMap,初始化的時候师幕,會拿到parent.InheritableThreadLocal粟按。直接上代碼可以看的很清楚。

class Thread {
    ...
    /* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;

    /*
     * InheritableThreadLocal values pertaining to this thread. This map is
     * maintained by the InheritableThreadLocal class.
     */
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
  
  ...
  
  if (inheritThreadLocals && parent.inheritableThreadLocals != null)
    this.inheritableThreadLocals =
    ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
}

JDKInheritableThreadLocal類可以完成父線程到子線程的值傳遞霹粥。但對于使用線程池等會池化復(fù)用線程的執(zhí)行組件的情況灭将,線程由線程池創(chuàng)建好,并且線程是池化起來反復(fù)使用的后控;這時父子線程關(guān)系的ThreadLocal值傳遞已經(jīng)沒有意義庙曙,應(yīng)用需要的實際上是把 任務(wù)提交給線程池時ThreadLocal值傳遞到 任務(wù)執(zhí)行時

2浩淘、日志MDC/Opentracing的實現(xiàn)

如果你的應(yīng)用實現(xiàn)了Opentracing的規(guī)范捌朴,比如通過skywalking的agent對線程池做了攔截,那么自定義Scope實現(xiàn)類张抄,可以跨線程傳遞MDC砂蔽,然后你的義務(wù)可以通過設(shè)置MDC的值,傳遞給子線程署惯。

代碼如下:

        this.scopeManager = scopeManager;
        this.wrapped = wrapped;
        this.finishOnClose = finishOnClose;
        this.toRestore = (OwlThreadLocalScope)scopeManager.tlsScope.get();
        scopeManager.tlsScope.set(this);
        if (wrapped instanceof JaegerSpan) {
            this.insertMDC(((JaegerSpan)wrapped).context());
        } else if (wrapped instanceof JaegerSpanWrapper) {
            this.insertMDC(((JaegerSpanWrapper)wrapped).getDelegated().context());
        }

3左驾、阿里transmittable-thread-local

github地址:https://github.com/alibaba/transmittable-thread-local

TransmittableThreadLocal(TTL)是框架/中間件缺少的Java?std lib(簡單和0依賴),提供了增強(qiáng)的InheritableThreadLocal极谊,即使使用線程池組件也可以在線程之間傳輸值诡右。

3.1 transmittable-thread-local 官方readme參考:

使用類TransmittableThreadLocal來保存值,并跨線程池傳遞轻猖。

TransmittableThreadLocal繼承InheritableThreadLocal帆吻,使用方式也類似。相比InheritableThreadLocal咙边,添加了

  1. copy方法
    用于定制 任務(wù)提交給線程池時ThreadLocal值傳遞到 任務(wù)執(zhí)行時 的拷貝行為猜煮,缺省傳遞的是引用。
    注意:如果跨線程傳遞了對象引用因為不再有線程封閉败许,與InheritableThreadLocal.childValue一樣友瘤,使用者/業(yè)務(wù)邏輯要注意傳遞對象的線程
  1. protectedbeforeExecute/afterExecute方法
    執(zhí)行任務(wù)(Runnable/Callable)的前/后的生命周期回調(diào),缺省是空操作檐束。

3.2 transmittable-thread-local 代碼例子

方式一:TtlRunnable封裝:

ExecutorService executorService = Executors.newCachedThreadPool();
TransmittableThreadLocal<String> context = new TransmittableThreadLocal<>();

// =====================================================
// 在父線程中設(shè)置
context.set("value-set-in-parent");

// 額外的處理辫秧,生成修飾了的對象ttlRunnable
Runnable ttlRunnable = TtlRunnable.get(() -> {
    System.out.println(context.get());
});
executorService.submit(ttlRunnable);

方式二:ExecutorService封裝:

ExecutorService executorService = ...
// 額外的處理,生成修飾了的對象executorService
executorService = TtlExecutors.getTtlExecutorService(executorService);

方式三:使用java agent被丧,無代碼入侵

這種方式盟戏,實現(xiàn)線程池的傳遞是透明的绪妹,業(yè)務(wù)代碼中沒有修飾Runnable或是線程池的代碼。即可以做到應(yīng)用代碼 無侵入柿究。

ExecutorService executorService = Executors.newCachedThreadPool();
TransmittableThreadLocal<String> context = new TransmittableThreadLocal<>();
// =====================================================
// 在父線程中設(shè)置
context.set("value-set-in-parent");

executorService.submit(() -> {
    System.out.println(context.get());
});

4邮旷、grpc的實現(xiàn)

grpc是一種分布式調(diào)用協(xié)議和實現(xiàn),也封裝了一套跨線程傳遞上下文的實現(xiàn)蝇摸。

io.grpc.Context 表示上下文婶肩,用來在一次grpc請求鏈路中傳遞用戶登錄信息、tracing信息等貌夕。

Context常用用法如下律歼。首先獲取當(dāng)前context,這個一般是作為參數(shù)傳過來的,或通過current()獲取當(dāng)前的已有context啡专。

然后通過attach方法险毁,綁定到當(dāng)前線程上,并且返回當(dāng)前線程

    public Runnable wrap(final Runnable r) {
        return new Runnable() {
            @Override
            public void run() {
                Context previous = attach();
                try {
                    r.run();
                } finally {
                    detach(previous);
                }
            }
        };
    }

Context的主要方法如下

  • attach() attach Context自己们童,從而進(jìn)入到一個新的scope中畔况,新的scope以此Context實例作為current,并且返回之前的current context
  • detach(Context toDetach) attach()方法的反向方法慧库,退出當(dāng)前Context并且detach到toDetachContext跷跪,每個attach方法要對應(yīng)一個detach,所以一般通過try finally代碼塊或wrap模板方法來使用齐板。
  • static storage() 獲取storage吵瞻,Storage是用來attach和detach當(dāng)前context用的。

線程池傳遞實現(xiàn):

ExecutorService executorService = Executors.newCachedThreadPool();
Context.withValue("key","value");

execute(Context.current().wrap(() -> {
            System.out.println(Context.current().getValue("key"));
        }));

5覆积、總結(jié)

以上總結(jié)的四種實現(xiàn)跨線程傳遞的方法听皿,最簡單的就是自己定義一個Runnable熟呛,添加屬性傳遞即可宽档。如果考慮通用型,需要中間件封裝一個Executor對象庵朝,類似transmittable-thread-local的實現(xiàn)吗冤,或者直接使用transmittable-thread-local。

實踐的項目中九府,考慮周全椎瘟,要支持spanMDC侄旬、rpc上下文肺蔚、業(yè)務(wù)自定義上下文,可以參考以上方法封裝儡羔。

參考資料

[grpc源碼分析1-context] https://www.codercto.com/a/66559.html

[threadlocal變量透傳宣羊,這些問題你都遇到過嗎璧诵?]https://cloud.tencent.com/developer/article/1492379

本文由猿必過 YBG 發(fā)布
禁止未經(jīng)授權(quán)轉(zhuǎn)載,違者依法追究相關(guān)法律責(zé)任
如需授權(quán)可聯(lián)系:zhuyunhui@yuanbiguo.com

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末仇冯,一起剝皮案震驚了整個濱河市之宿,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌苛坚,老刑警劉巖比被,帶你破解...
    沈念sama閱讀 218,858評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異泼舱,居然都是意外死亡等缀,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,372評論 3 395
  • 文/潘曉璐 我一進(jìn)店門柠掂,熙熙樓的掌柜王于貴愁眉苦臉地迎上來项滑,“玉大人,你說我怎么就攤上這事涯贞∏箍瘢” “怎么了?”我有些...
    開封第一講書人閱讀 165,282評論 0 356
  • 文/不壞的土叔 我叫張陵宋渔,是天一觀的道長州疾。 經(jīng)常有香客問我,道長皇拣,這世上最難降的妖魔是什么严蓖? 我笑而不...
    開封第一講書人閱讀 58,842評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮氧急,結(jié)果婚禮上颗胡,老公的妹妹穿的比我還像新娘。我一直安慰自己吩坝,他們只是感情好毒姨,可當(dāng)我...
    茶點故事閱讀 67,857評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著钉寝,像睡著了一般弧呐。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上嵌纲,一...
    開封第一講書人閱讀 51,679評論 1 305
  • 那天俘枫,我揣著相機(jī)與錄音,去河邊找鬼逮走。 笑死鸠蚪,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播茅信,決...
    沈念sama閱讀 40,406評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼酣栈,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了汹押?” 一聲冷哼從身側(cè)響起矿筝,我...
    開封第一講書人閱讀 39,311評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎棚贾,沒想到半個月后窖维,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,767評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡妙痹,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年铸史,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片怯伊。...
    茶點故事閱讀 40,090評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡琳轿,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出耿芹,到底是詐尸還是另有隱情崭篡,我是刑警寧澤,帶...
    沈念sama閱讀 35,785評論 5 346
  • 正文 年R本政府宣布吧秕,位于F島的核電站琉闪,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏砸彬。R本人自食惡果不足惜颠毙,卻給世界環(huán)境...
    茶點故事閱讀 41,420評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望砂碉。 院中可真熱鬧蛀蜜,春花似錦、人聲如沸增蹭。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,988評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽沪铭。三九已至壮池,卻和暖如春偏瓤,著一層夾襖步出監(jiān)牢的瞬間杀怠,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,101評論 1 271
  • 我被黑心中介騙來泰國打工厅克, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留赔退,地道東北人。 一個月前我還...
    沈念sama閱讀 48,298評論 3 372
  • 正文 我出身青樓,卻偏偏與公主長得像硕旗,于是被迫代替她去往敵國和親窗骑。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,033評論 2 355

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