ThreadLocal?在上下文傳值場景實(shí)踐源碼
開篇語
我們在 《打動面試官:線程池流程編排中的運(yùn)用實(shí)戰(zhàn)》一文中將流程引擎簡單地完善了一下,本文在其基礎(chǔ)上繼續(xù)進(jìn)行改造,建議同學(xué)可以先看看 GitHub 上的代碼,或者看看之前的文章。
1、回顧
流程引擎編排的對象,我們稱為組件(就是 SpringBean),之前我們給組件定義了通用的接口,組件實(shí)現(xiàn)時(shí)就實(shí)現(xiàn)這個(gè)接口,代碼如下:

我們定義了 DomainAbilityBean 接口,入?yún)⒑统鰠⒍际?FlowContent,F(xiàn)lowContent 我們稱為上下文。
2、ThreadLocal 實(shí)現(xiàn)
上下文傳參除了 FlowContent 實(shí)現(xiàn)外,ThreadLocal 也是可以實(shí)現(xiàn)的,我們來演示一下:
2.1、定義 ThreadLocal 上下文工具類
首先我們使用 ThreadLocal 定義了上下文工具類,并且定義了 put、get 方法,方便使用,代碼如下:
public class ContextCache implements Serializable {
private static final long serialVersionUID = 2136539028591849277L;
// 使用 ThreadLocal 緩存上下文信息
public static final ThreadLocal<Map<String,String>> CACHE = new ThreadLocal<>();
/**
* 放數(shù)據(jù)
* @param sourceKey
*/
public static final void putAttribute(String sourceKey,String value){
Map<String,String> cacheMap = CACHE.get();
if(null == cacheMap){
cacheMap = new HashMap<>();
}
cacheMap.put(sourceKey,value);
CACHE.set(cacheMap);
}
/**
* 拿數(shù)據(jù)
* @param sourceKey
*/
public static final String getAttribute(String sourceKey){
Map<String,String> cacheMap = CACHE.get();
if(null == cacheMap){
return null;
}
return cacheMap.get(sourceKey);
}
}如果你想往 ThreadLocal 放數(shù)據(jù),調(diào)用 ContextCache.putAttribute 方法,如果想從 ThreadLocal 拿數(shù)據(jù),調(diào)用 ContextCache.getAttribute 方法即可。
我們寫了兩個(gè)組件,一個(gè)組件放數(shù)據(jù),一個(gè)組件拿數(shù)據(jù),如下:

我們把兩個(gè) SpringBean 注冊到流程注冊中心中,讓其按照先執(zhí)行 BeanThree 再執(zhí)行 BeanFive 的順序進(jìn)行執(zhí)行,運(yùn)行 DemoApplication 類的 main 方法進(jìn)行執(zhí)行,執(zhí)行結(jié)果如下:

從打印的日志可以看到,在 Spring 容器管理的 SpringBean 中,ThreadLocal 也是可以儲存中間緩存值的。
3、開啟子線程
我們做一個(gè)實(shí)驗(yàn),我們在 BeanFive 中開啟子線程,然后再從 ThreadLocal 中拿值,看看能否拿到值,BeanFive 的代碼修改成如下:

我們再來運(yùn)行一下,打印的日志如下:

從打印的日志中,我們發(fā)現(xiàn)在子線程中從 ThreadLocal 取值時(shí),并沒有取得值,這個(gè)原因主要是我們之前說的,線程在創(chuàng)建的時(shí)候,并不會把父線程的 ThreadLocal 中的值拷貝給子線程的 ThreadLocal,解決方案就是把 ThreadLocal 修改成 InheritableThreadLocal,代碼修改如下:

我們再次運(yùn)行,結(jié)果如下:

從運(yùn)行結(jié)果看,我們成功的在子線程中拿到值。
4、線程池 + ThreadLocal
如果是拿數(shù)據(jù)的 springBean 是丟給線程池執(zhí)行的,我們能夠成功的從 ThreadLocal 中拿到數(shù)據(jù)么?
首先我們在放數(shù)據(jù)的 springBean 中,把放的值修改成隨機(jī)的,接著拿數(shù)據(jù)的 SpringBean 修改成異步執(zhí)行,代碼修改如下:

為了能快速看到效果,我們把線程池的 coreSize 和 maxSize 全部修改成 3,并讓任務(wù)沉睡一段時(shí)間,這樣三個(gè)線程肯定消費(fèi)不完任務(wù),大量任務(wù)都會到隊(duì)列中去排隊(duì),我們修改一下測試腳本,如下:

我們期望的結(jié)果:
線程池中執(zhí)行的 BeanFive 可以成功從 ThreadLocal 中拿到數(shù)據(jù);能夠從 ThreadLocal 拿到正確的數(shù)據(jù),比如 BeanThree 剛放進(jìn) key1,value5,那么期望在 BeanFive 中根據(jù) key1 能拿出 value5,而不是其它值。
我們運(yùn)行一下,結(jié)果如下:

從結(jié)果中可以看到,并沒有符合我們的預(yù)期,我們往 ThreadLocal 中 put 進(jìn)很多值,但最后拿出來的值卻很多都是 value379,都為最后 put 到 ThreadLocal 中的值。
這個(gè)原因主要是 ThreadLocal 存儲的 HashMap 的引用都是同一個(gè),main 主線程可以修改 HashMap 中的值,子線程從 ThreadLocal 中拿值時(shí),也是從 HashMap 中拿值,從而導(dǎo)致不能把 put 的值通過 ThreadLocal 正確的傳遞給子線程。
為了證明是這個(gè)原因,我們在從 ThreadLocal 放、拿值的地方,把 HashMap 的內(nèi)存地址都打印出來,改動代碼如下:

我們再次運(yùn)行測試代碼,運(yùn)行的結(jié)果如下:

從測試結(jié)果中可以看到,不管是主線程還是子線程和 ThreadLocal 進(jìn)行交互時(shí),HashMap 都是同一個(gè),也就是說 ThreadLocal 中保存的 HashMap 是共享的,這就導(dǎo)致了線程安全的問題,子線程讀取到的值就會混亂掉。
5、解決方案
針對這個(gè)問題,我們提出了一種解決方案,在把任務(wù)提交到線程池時(shí),我們進(jìn)行 HashMap 的拷貝,這樣子線程的 HashMap 和 main 線程的 HashMap 就不同了,可以解決上面的問題。
我們提交任務(wù)時(shí), 使用的是 Runnable,要實(shí)現(xiàn) HashMap 的拷貝的話,我們需要把 Runnable 進(jìn)行一層包裝,包裝的代碼如下:

運(yùn)行結(jié)果如下:

從運(yùn)行結(jié)果中可以看出,線程池拿出來的 value 已經(jīng)是正確的了。
6、總結(jié)
本文通過 ThreadLocal 來改造流程引擎中的上下文傳遞,希望能夠加深大家對 ThreadLocal 的認(rèn)識和使用技巧,有興趣的同學(xué)可以把我們的代碼下載下來,跑跑看。
以上就是ThreadLocal 在上下文傳值場景下的實(shí)踐的詳細(xì)內(nèi)容,更多關(guān)于ThreadLocal 上下文傳值場景實(shí)踐的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
淺談mybatis-plus批量保存異常及效率優(yōu)化
本文主要介紹了mybatis-plus批量保存異常及效率優(yōu)化,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2024-01-01
Spring學(xué)習(xí)筆記之RedisTemplate的配置與使用教程
這篇文章主要給大家介紹了關(guān)于Spring學(xué)習(xí)筆記之RedisTemplate配置與使用的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對大家學(xué)習(xí)或者使用spring具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2018-06-06
mybatis中實(shí)現(xiàn)枚舉自動轉(zhuǎn)換方法詳解
在使用mybatis的時(shí)候經(jīng)常會遇到枚舉類型的轉(zhuǎn)換,下面這篇文章主要給大家介紹了關(guān)于mybatis中實(shí)現(xiàn)枚舉自動轉(zhuǎn)換的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來一起看看吧。2017-08-08
Spring項(xiàng)目中使用Cache?Redis實(shí)現(xiàn)數(shù)據(jù)緩存
這篇文章主要為大家介紹了項(xiàng)目中使用Spring?Cache?Redis實(shí)現(xiàn)數(shù)據(jù)緩存,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-06-06
Java實(shí)現(xiàn)時(shí)間日期格式轉(zhuǎn)換示例
本篇文章主要介紹了ava實(shí)現(xiàn)時(shí)間日期格式轉(zhuǎn)換示例,實(shí)現(xiàn)了各種時(shí)間輸出的類型,有興趣的可以了解一下。2017-01-01
J2ee 高并發(fā)情況下監(jiān)聽器實(shí)例詳解
這篇文章主要介紹了J2ee 高并發(fā)情況下監(jiān)聽器實(shí)例詳解的相關(guān)資料,需要的朋友可以參考下2017-02-02
Eclipse導(dǎo)入項(xiàng)目報(bào)錯(cuò)問題解決方案
這篇文章主要介紹了Eclipse導(dǎo)入項(xiàng)目報(bào)錯(cuò)問題解決方案,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-07-07
解決IDEA?2022?Translation?翻譯文檔失敗:?未知錯(cuò)誤的問題
這篇文章主要介紹了IDEA?2022?Translation?翻譯文檔失敗:?未知錯(cuò)誤,本文較詳細(xì)的給大家介紹了IDEA?2022?Translation未知錯(cuò)誤翻譯文檔失敗的解決方法,需要的朋友可以參考下2022-04-04

