你的也是我的。3例ko多線程,局部變量透傳
java中的threadlocal,是綁定在線程上的。你在一個線程中set的值,在另外一個線程是拿不到的。如果在threadlocal的平行線程中,創(chuàng)建了新的子線程,那么這里面的值是無法傳遞、共享的(先想清楚為什么再往下看)。這就是透傳問題。
值在線程之間的透傳,你可以認為是一個bug,這些問題一般會比較隱蔽,但問題暴露的時候脾氣卻比較火爆,讓人手忙腳亂,懷疑人生。
作為代碼的掌舵者,我們必然不能忍受這種問題的蹂躪。本篇文章適合細看,我們拿出3個例子,通過編碼手段說明解決此類bug的通用方式,希望能達到舉一反三的效果。對于搞基礎架構(gòu)的同學,是必備知識點。
1、普通線程的ThreadLocal透傳問題
2、sl4j MDC組件中ThreadLocal透傳問題
3、Hystrix組件的透傳問題
由于涉及代碼比較多,xjjdog將這三個例子的代碼,放在了github上,想深入研究,可以下載下來debug一下。
- https://github.com/xjjdog/example-pass-through
一、問題簡單演示
為了有個比較直觀的認識,下面展示一段異常代碼。
以上代碼在主線程設置了一個簡單的threadlocal變量,然后在自線程中想要取出它的值。執(zhí)行后發(fā)現(xiàn),程序的輸出是:null。
程序的輸出和我們的期望產(chǎn)生了明顯的差異。其實,將ThreadLocal 換成InheritableThreadLocal 就ok了。不要高興太早,對于使用線程池的情況,由于會緩存線程,線程是緩存起來反復使用的。這時父子線程關系的上下文傳遞,已經(jīng)沒有意義。
二、解決線程池透傳問題
所以,線程池InheritableThreadLocal進行提交,獲取的值,有可能是前一個任務執(zhí)行后留下的,是錯誤的。使用只有在任務執(zhí)行的時候進行傳遞,才是正常的功能。
上面的問題,transmittable-thread-local項目,已經(jīng)很好的解決,并提供了java-agent的方式支持。
我們這里從最小集合的源碼層面,來看一下其中的內(nèi)容。首先,我們看一下ThreadLocal的結(jié)構(gòu)。
ThreadLocal其實是作為一個Map中的key而存在的,這個Map就是ThreadLocalMap,它以私有變量的形式,存在于Thread類中。拿上圖為例,如果我創(chuàng)建了一個ThreadLocal,然后調(diào)用set方法,它會首先找到當前的thread,然后找到threadLocals,最后把自己作為key,存放在這個map里。
- hread t = Thread.currentThread();
- ThreadLocalMap map = getMap(t);
- map.set(this, value);
要能夠完成多線程的協(xié)調(diào)工作,必須提供全套的多線程工具。包括但不限于:
1、定義注解,以及被注解修飾的ThreadLocal類
定義新的ThreadLocal類,以便在賦值的時候,能夠根據(jù)注解進行攔截和過濾。這就要求,在定義ThreadLocal的時候,要使用我們提供的ThreadLocal類,而不是jdk提供的那兩個。
2、進行父子線程之間的數(shù)據(jù)拷貝
在線程池提交任務之前,我們需要有個地方,將父進程的ThreadLocal內(nèi)容,暫存一下。
由于很多變量都是private的,需要根據(jù)反射進行操作。根據(jù)上面提供的ThreadLocal類的結(jié)構(gòu),我們需要直接操作其中的變量table(這也是為什么jdk不能隨便改變變量名的原因)。
將父線程相關的變量暫存之后,就可以在使用的時候,通過主動設值和清理,完成變量拷貝。
3、提供專用的Callable或者Runnable
那么這些數(shù)據(jù)是如何組裝起來的呢?還是靠我們的任務載體類。
線程池提交線程,一般是通過Callable或者Runnable,以Runnable為例,我們看一下這個調(diào)用關系。
以下類采用了委托模式。
這樣,只要在提交任務的時候,使用了我們自定義的Runnable;同時,使用了自定義的ThreadLocal,就能夠正常完成透傳。
三、解決MDC透傳問題
sl4j MDC機制非常好,通常用于保存線程本地的“診斷數(shù)據(jù)”然后有日志組件打印,其內(nèi)部時基于threadLocal實現(xiàn);不過這就有一些問題,主線程中設置的MDC數(shù)據(jù),在其子線程(多線程池)中是無法獲取的,下面就來介紹如何解決這個問題。
!MDC ( Mapped Diagnostic Contexts ),它是一個線程安全的存放診斷日志的容器。通常,會在處理請求前將請求的唯一標示放到MDC容器中,比如sessionId。這個唯一標示會隨著日志一起輸出。配置文件可以使用占位符進行變量替換。
類似于上面介紹的方式,我們需要提供專用的Callable和Runnable。另外,為了能夠同時支持MDC和普通線程,這兩個類采用裝飾器模式,進行功能追加。就單個類來說,對外的展現(xiàn)依然是委托模式。
同樣的思路,同樣的模式。不一樣的是,父線程的信息暫存,我們直接使用MDC的內(nèi)部方法,并在任務的執(zhí)行前后,進行相應操作。
四、解決Hystrix透傳問題
同樣的問題,在Netflix公司的熔斷組件Hystrix中,依然存在。Hystrix線程池模式下,透傳ThreadLocal需要進行改造,它本身是無法完成這個功能的。
但是Hystrix策略無法簡單通過yml文件方式配置。我們參考Spring Cloud中對此策略的擴展方式,開發(fā)自己的策略。需要繼承HystrixConcurrentStrategy。
構(gòu)造代碼還是較長的,可以查看github項目。但有一個地方需要說明。
我們使用裝飾器模式,對代碼進行了層層嵌套,同時將多線程透傳功能、MDC傳遞功能給追加了進來。這樣,我們的這個類,就同時在以上三個環(huán)境中擁有了透傳功能。
End
同樣的思路,可以用在其他組件上。比如我們在多篇調(diào)用鏈的文章里,提到的trace信息在多線程環(huán)境下的傳遞。
一般就是在當前線程暫存數(shù)據(jù),然后在提交任務時進行包裝。值得注意的是,這種方式侵入性還是比較大的,適合封裝在通用的基礎工具包中。你要是在業(yè)務中這么用,大概率會被罵死。
那可如何是好。
ThreadLocal會引發(fā)很多棘手的bug,造成代碼污染。在使用之前,一定要確保你確實需要使用它。比如你在SimpleDateFormat類上用了線程局部變量,可以將它替換成DateTimeFormatter。
我們不善于解決問題,我們只善于解決容易出問題的類。
作者簡介:小姐姐味道 (xjjdog),一個不允許程序員走彎路的公眾號。聚焦基礎架構(gòu)和Linux。十年架構(gòu),日百億流量,與你探討高并發(fā)世界,給你不一樣的味道。我的個人微信xjjdog0,歡迎添加好友,進一步交流。