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