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