用這4招 優雅的實現Spring Boot 非同步執行緒間資料傳遞
大家好,我是不才陳某~
Spring Boot 自定義執行緒池實現非同步開發相信看過陳某的文章都瞭解,但是在實際開發中需要在父子執行緒之間傳遞一些資料,比如使用者資訊,鏈路資訊等等
比如使用者登入資訊使用ThreadLocal存放保證執行緒隔離,程式碼如下:
/**
* @author 公眾號:碼猿技術專欄
* @description 使用者上下文資訊
*/
public class OauthContext {
private static final ThreadLocal<LoginVal> loginValThreadLocal=new ThreadLocal<>();
public static LoginVal get(){
return loginValThreadLocal.get();
}
public static void set(LoginVal loginVal){
loginValThreadLocal.set(loginVal);
}
public static void clear(){
loginValThreadLocal.remove();
}
}
那麼子執行緒想要獲取這個LoginVal如何做呢?
今天就來介紹幾種優雅的方式實現Spring Boot 內部的父子執行緒的資料傳遞。
1. 手動設定
每執行一次非同步執行緒都要分為兩步:
獲取父執行緒的LoginVal 將LoginVal設定到子執行緒,達到複用
程式碼如下:
public void handlerAsync() {
//1. 獲取父執行緒的loginVal
LoginVal loginVal = OauthContext.get();
log.info("父執行緒的值:{}",OauthContext.get());
CompletableFuture.runAsync(()->{
//2. 設定子執行緒的值,複用
OauthContext.set(loginVal);
log.info("子執行緒的值:{}",OauthContext.get());
});
}
雖然能夠實現目的,但是每次開非同步執行緒都需要手動設定,重複程式碼太多,看了頭疼,你認為優雅嗎?
2. 執行緒池設定TaskDecorator
TaskDecorator是什麼?官方api的大致意思:這是一個執行回撥方法的裝飾器,主要應用於傳遞上下文,或者提供任務的監控/統計資訊。
知道有這麼一個東西,如何去使用?
TaskDecorator是一個介面,首先需要去實現它,程式碼如下:
/**
* @author 公眾號:碼猿技術專欄
* @description 上下文裝飾器
*/
public class ContextTaskDecorator implements TaskDecorator {
@Override
public Runnable decorate(Runnable runnable) {
//獲取父執行緒的loginVal
LoginVal loginVal = OauthContext.get();
return () -> {
try {
// 將主執行緒的請求資訊,設定到子執行緒中
OauthContext.set(loginVal);
// 執行子執行緒,這一步不要忘了
runnable.run();
} finally {
// 執行緒結束,清空這些資訊,否則可能造成記憶體洩漏
OauthContext.clear();
}
};
}
}
這裡我只是設定了LoginVal,實際開發中其他的共享資料,比如SecurityContext
,RequestAttributes
....
TaskDecorator
需要結合執行緒池使用,實際開發中非同步執行緒建議使用執行緒池,只需要在對應的執行緒池配置一下,程式碼如下:
@Bean("taskExecutor")
public ThreadPoolTaskExecutor taskExecutor() {
ThreadPoolTaskExecutor poolTaskExecutor = new ThreadPoolTaskExecutor();
poolTaskExecutor.setCorePoolSize(xx);
poolTaskExecutor.setMaxPoolSize(xx);
// 設定執行緒活躍時間(秒)
poolTaskExecutor.setKeepAliveSeconds(xx);
// 設定佇列容量
poolTaskExecutor.setQueueCapacity(xx);
//設定TaskDecorator,用於解決父子執行緒間的資料複用
poolTaskExecutor.setTaskDecorator(new ContextTaskDecorator());
poolTaskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
// 等待所有任務結束後再關閉執行緒池
poolTaskExecutor.setWaitForTasksToCompleteOnShutdown(true);
return poolTaskExecutor;
}
此時業務程式碼就不需要去設定子執行緒的值,直接使用即可,程式碼如下:
public void handlerAsync() {
log.info("父執行緒的使用者資訊:{}", OauthContext.get());
//執行非同步任務,需要指定的執行緒池
CompletableFuture.runAsync(()-> log.info("子執行緒的使用者資訊:{}", OauthContext.get()),taskExecutor);
}
來看一下結果,如下圖:
這裡使用的是CompletableFuture
執行非同步任務,使用@Async
這個註解同樣是可行的。
注意:無論使用何種方式,都需要指定執行緒池
3. InheritableThreadLocal
這種方案不建議使用,InheritableThreadLocal雖然能夠實現父子執行緒間的複用,但是線上程池中使用會存在複用的問題,具體的可以看陳某之前的文章:微服務中使用阿里開源的TTL,優雅的實現身份資訊的執行緒間複用
這種方案使用也是非常簡單,直接用InheritableThreadLocal替換ThreadLocal即可,程式碼如下:
/**
* @author 公眾號:碼猿技術專欄
* @description 使用者上下文資訊
*/
public class OauthContext {
private static final InheritableThreadLocal<LoginVal> loginValThreadLocal=new InheritableThreadLocal<>();
public static LoginVal get(){
return loginValThreadLocal.get();
}
public static void set(LoginVal loginVal){
loginValThreadLocal.set(loginVal);
}
public static void clear(){
loginValThreadLocal.remove();
}
}
4. TransmittableThreadLocal
TransmittableThreadLocal是阿里開源的工具,彌補了InheritableThreadLocal的缺陷,在使用執行緒池等會池化複用執行緒的執行元件情況下,提供ThreadLocal
值的傳遞功能,解決非同步執行時上下文傳遞的問題。
使用起來也是非常簡單,新增依賴如下:
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>transmittable-thread-local</artifactId>
<version>2.14.2</version>
</dependency>
OauthContext改造程式碼如下:
/**
* @author 公眾號:碼猿技術專欄
* @description 使用者上下文資訊
*/
public class OauthContext {
private static final TransmittableThreadLocal<LoginVal> loginValThreadLocal=new TransmittableThreadLocal<>();
public static LoginVal get(){
return loginValThreadLocal.get();
}
public static void set(LoginVal loginVal){
loginValThreadLocal.set(loginVal);
}
public static void clear(){
loginValThreadLocal.remove();
}
}
關於TransmittableThreadLocal想深入瞭解其原理可以看陳某之前的文章:微服務中使用阿里開源的TTL,優雅的實現身份資訊的執行緒間複用,應用還是非常廣泛的
TransmittableThreadLocal原理
從定義來看,TransimittableThreadLocal
繼承於InheritableThreadLocal
,並實現TtlCopier
介面,它裡面只有一個copy
方法。所以主要是對InheritableThreadLocal
的擴充套件。
public class TransmittableThreadLocal<T> extends InheritableThreadLocal<T> implements TtlCopier<T>
在TransimittableThreadLocal
中新增holder
屬性。這個屬性的作用就是被標記為具備執行緒傳遞資格的物件都會被新增到這個物件中。
要標記一個類,比較容易想到的方式,就是給這個類新增一個Type
欄位,還有一個方法就是將具備這種型別的的物件都新增到一個靜態全域性集合中。之後使用時,這個集合裡的所有值都具備這個標記。
// 1. holder本身是一個InheritableThreadLocal物件
// 2. 這個holder物件的value是WeakHashMap<TransmittableThreadLocal<Object>, ?>
// 2.1 WeekHashMap的value總是null,且不可能被使用。
// 2.2 WeekHasshMap支援value=null
private static InheritableThreadLocal<WeakHashMap<TransmittableThreadLocal<Object>, ?>> holder = new InheritableThreadLocal<WeakHashMap<TransmittableThreadLocal<Object>, ?>>() {
@Override
protected WeakHashMap<TransmittableThreadLocal<Object>, ?> initialValue() {
return new WeakHashMap<TransmittableThreadLocal<Object>, Object>();
}
/**
* 重寫了childValue方法,實現上直接將父執行緒的屬性作為子執行緒的本地變數物件。
*/
@Override
protected WeakHashMap<TransmittableThreadLocal<Object>, ?> childValue(WeakHashMap<TransmittableThreadLocal<Object>, ?> parentValue) {
return new WeakHashMap<TransmittableThreadLocal<Object>, Object>(parentValue);
}
};
應用程式碼是透過TtlExecutors
工具類對執行緒池物件進行包裝。工具類只是簡單的判斷,輸入的執行緒池是否已經被包裝過、非空校驗等,然後返回包裝類ExecutorServiceTtlWrapper
。根據不同的執行緒池型別,有不同和的包裝類。
@Nullable
public static ExecutorService getTtlExecutorService(@Nullable ExecutorService executorService) {
if (TtlAgent.isTtlAgentLoaded() || executorService == null || executorService instanceof TtlEnhanced) {
return executorService;
}
return new ExecutorServiceTtlWrapper(executorService);
}
進入包裝類ExecutorServiceTtlWrapper
。可以注意到不論是透過ExecutorServiceTtlWrapper#submit
方法或者是ExecutorTtlWrapper#execute
方法,都會將執行緒物件包裝成TtlCallable
或者TtlRunnable
,用於在真正執行run
方法前做一些業務邏輯。
/**
* 在ExecutorServiceTtlWrapper實現submit方法
*/
@NonNull
@Override
public <T> Future<T> submit(@NonNull Callable<T> task) {
return executorService.submit(TtlCallable.get(task));
}
/**
* 在ExecutorTtlWrapper實現execute方法
*/
@Override
public void execute(@NonNull Runnable command) {
executor.execute(TtlRunnable.get(command));
}
所以,重點的核心邏輯應該是在TtlCallable#call()
或者TtlRunnable#run()
中。以下以TtlCallable
為例,TtlRunnable
同理類似。在分析call()
方法之前,先看一個類Transmitter
public static class Transmitter {
/**
* 捕獲當前執行緒中的是所有TransimittableThreadLocal和註冊ThreadLocal的值。
*/
@NonNull
public static Object capture() {
return new Snapshot(captureTtlValues(), captureThreadLocalValues());
}
/**
* 捕獲TransimittableThreadLocal的值,將holder中的所有值都新增到HashMap後返回。
*/
private static HashMap<TransmittableThreadLocal<Object>, Object> captureTtlValues() {
HashMap<TransmittableThreadLocal<Object>, Object> ttl2Value =
new HashMap<TransmittableThreadLocal<Object>, Object>();
for (TransmittableThreadLocal<Object> threadLocal : holder.get().keySet()) {
ttl2Value.put(threadLocal, threadLocal.copyValue());
}
return ttl2Value;
}
/**
* 捕獲註冊的ThreadLocal的值,也就是原本執行緒中的ThreadLocal,可以註冊到TTL中,在
* 進行執行緒池本地變數傳遞時也會被傳遞。
*/
private static HashMap<ThreadLocal<Object>, Object> captureThreadLocalValues() {
final HashMap<ThreadLocal<Object>, Object> threadLocal2Value =
new HashMap<ThreadLocal<Object>, Object>();
for(Map.Entry<ThreadLocal<Object>,TtlCopier<Object>>entry:threadLocalHolder.entrySet()){
final ThreadLocal<Object> threadLocal = entry.getKey();
final TtlCopier<Object> copier = entry.getValue();
threadLocal2Value.put(threadLocal, copier.copy(threadLocal.get()));
}
return threadLocal2Value;
}
/**
* 將捕獲到的本地變數進行替換子執行緒的本地變數,並且返回子執行緒現有的本地變數副本backup。
* 用於在執行run/call方法之後,將本地變數副本恢復。
*/
@NonNull
public static Object replay(@NonNull Object captured) {
final Snapshot capturedSnapshot = (Snapshot) captured;
return new Snapshot(replayTtlValues(capturedSnapshot.ttl2Value),
replayThreadLocalValues(capturedSnapshot.threadLocal2Value));
}
/**
* 替換TransmittableThreadLocal
*/
@NonNull
private static HashMap<TransmittableThreadLocal<Object>, Object> replayTtlValues(@NonNull HashMap<TransmittableThreadLocal<Object>, Object> captured) {
// 建立副本backup
HashMap<TransmittableThreadLocal<Object>, Object> backup =
new HashMap<TransmittableThreadLocal<Object>, Object>();
for (final Iterator<TransmittableThreadLocal<Object>> iterator = holder.get().keySet().iterator(); iterator.hasNext(); ) {
TransmittableThreadLocal<Object> threadLocal = iterator.next();
// 對當前執行緒的本地變數進行副本複製
backup.put(threadLocal, threadLocal.get());
// 若出現呼叫執行緒中不存在某個執行緒變數,而執行緒池中執行緒有,則刪除執行緒池中對應的本地變數
if (!captured.containsKey(threadLocal)) {
iterator.remove();
threadLocal.superRemove();
}
}
// 將捕獲的TTL值打入執行緒池獲取到的執行緒TTL中。
setTtlValuesTo(captured);
// 是一個擴充套件點,呼叫TTL的beforeExecute方法。預設實現為空
doExecuteCallback(true);
return backup;
}
private static HashMap<ThreadLocal<Object>, Object> replayThreadLocalValues(@NonNull HashMap<ThreadLocal<Object>, Object> captured) {
final HashMap<ThreadLocal<Object>, Object> backup =
new HashMap<ThreadLocal<Object>, Object>();
for (Map.Entry<ThreadLocal<Object>, Object> entry : captured.entrySet()) {
final ThreadLocal<Object> threadLocal = entry.getKey();
backup.put(threadLocal, threadLocal.get());
final Object value = entry.getValue();
if (value == threadLocalClearMark) threadLocal.remove();
else threadLocal.set(value);
}
return backup;
}
/**
* 清除單線執行緒的所有TTL和TL,並返回清除之氣的backup
*/
@NonNull
public static Object clear() {
final HashMap<TransmittableThreadLocal<Object>, Object> ttl2Value =
new HashMap<TransmittableThreadLocal<Object>, Object>();
final HashMap<ThreadLocal<Object>, Object> threadLocal2Value =
new HashMap<ThreadLocal<Object>, Object>();
for(Map.Entry<ThreadLocal<Object>,TtlCopier<Object>>entry:threadLocalHolder.entrySet()){
final ThreadLocal<Object> threadLocal = entry.getKey();
threadLocal2Value.put(threadLocal, threadLocalClearMark);
}
return replay(new Snapshot(ttl2Value, threadLocal2Value));
}
/**
* 還原
*/
public static void restore(@NonNull Object backup) {
final Snapshot backupSnapshot = (Snapshot) backup;
restoreTtlValues(backupSnapshot.ttl2Value);
restoreThreadLocalValues(backupSnapshot.threadLocal2Value);
}
private static void restoreTtlValues(@NonNull HashMap<TransmittableThreadLocal<Object>, Object> backup) {
// 擴充套件點,呼叫TTL的afterExecute
doExecuteCallback(false);
for (final Iterator<TransmittableThreadLocal<Object>> iterator = holder.get().keySet().iterator(); iterator.hasNext(); ) {
TransmittableThreadLocal<Object> threadLocal = iterator.next();
if (!backup.containsKey(threadLocal)) {
iterator.remove();
threadLocal.superRemove();
}
}
// 將本地變數恢復成備份版本
setTtlValuesTo(backup);
}
private static void setTtlValuesTo(@NonNull HashMap<TransmittableThreadLocal<Object>, Object> ttlValues) {
for (Map.Entry<TransmittableThreadLocal<Object>, Object> entry : ttlValues.entrySet()) {
TransmittableThreadLocal<Object> threadLocal = entry.getKey();
threadLocal.set(entry.getValue());
}
}
private static void restoreThreadLocalValues(@NonNull HashMap<ThreadLocal<Object>, Object> backup) {
for (Map.Entry<ThreadLocal<Object>, Object> entry : backup.entrySet()) {
final ThreadLocal<Object> threadLocal = entry.getKey();
threadLocal.set(entry.getValue());
}
}
/**
* 快照類,儲存TTL和TL
*/
private static class Snapshot {
final HashMap<TransmittableThreadLocal<Object>, Object> ttl2Value;
final HashMap<ThreadLocal<Object>, Object> threadLocal2Value;
private Snapshot(HashMap<TransmittableThreadLocal<Object>, Object> ttl2Value,
HashMap<ThreadLocal<Object>, Object> threadLocal2Value) {
this.ttl2Value = ttl2Value;
this.threadLocal2Value = threadLocal2Value;
}
}
進入TtlCallable#call()
方法。
@Override
public V call() throws Exception {
Object captured = capturedRef.get();
if (captured == null || releaseTtlValueReferenceAfterCall &&
!capturedRef.compareAndSet(captured, null)) {
throw new IllegalStateException("TTL value reference is released after call!");
}
// 呼叫replay方法將捕獲到的當前執行緒的本地變數,傳遞給執行緒池執行緒的本地變數,
// 並且獲取到執行緒池執行緒覆蓋之前的本地變數副本。
Object backup = replay(captured);
try {
// 執行緒方法呼叫
return callable.call();
} finally {
// 使用副本進行恢復。
restore(backup);
}
}
到這基本上執行緒池方式傳遞本地變數的核心程式碼已經大概看完了。總的來說在建立TtlCallable
物件是,呼叫capture()
方法捕獲呼叫方的本地執行緒變數,在call()
執行時,將捕獲到的執行緒變數,替換到執行緒池所對應獲取到的執行緒的本地變數中,並且在執行完成之後,將其本地變數恢復到呼叫之前。
總結
上述列舉了4種方案,陳某這裡推薦方案2和方案4,其中兩種方案的缺點非常明顯,實際開發中也是採用的方案2或者方案4
最後說一句(別白嫖,求關注)
陳某每一篇文章都是精心輸出,如果這篇文章對你有所幫助,或者有所啟發的話,幫忙點贊、在看、轉發、收藏,你的支援就是我堅持下去的最大動力!
另外陳某的知識星球開通了,公眾號回覆關鍵詞:知識星球 獲取限量30元優惠券加入,目前更新了Spring全家桶實戰系列、億級資料分庫分表實戰、DDD微服務實戰專欄、我要進大廠等....
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/70024420/viewspace-2930960/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- Spring Boot使用@Async實現非同步呼叫:ThreadPoolTaskScheduler執行緒池的優雅關閉Spring Boot非同步thread執行緒
- Spring Boot中如何優雅地實現非同步呼叫?Spring Boot非同步
- .NET非同步程式設計——給執行緒傳遞資料非同步程式設計執行緒
- Spring Boot使用@Async實現非同步呼叫:自定義執行緒池Spring Boot非同步執行緒
- (MFC)子執行緒的資料如何傳遞給主執行緒中?執行緒
- Flask中請求資料的優雅傳遞Flask
- 9.2 運用API實現執行緒同步API執行緒
- 執行緒組之間的JMeter傳遞變數執行緒JMeter變數
- android 主執行緒和子執行緒之間的訊息傳遞Android執行緒
- c++ 執行緒函式傳遞資料 namespaceC++執行緒函式namespace
- bundle實現Activity之間的資料傳遞
- 非同步/同步,阻塞/非阻塞,單執行緒/多執行緒概念梳理非同步執行緒
- 如何優雅的使用執行緒池執行緒
- 安全優雅地停止執行緒執行緒
- 優雅關閉執行緒池的方案執行緒
- 如何優雅的停止一個執行緒?執行緒
- 如何優雅地停止 Spring Boot 應用?Spring Boot
- Spring Boot執行緒安全指南Spring Boot執行緒
- Laravel 應用實踐:如何優雅的完成全量資料同步Laravel
- Java優雅關閉執行緒池Java執行緒
- 探索:優雅地實現非同步方法的並行化非同步並行
- c++ 執行緒函式傳遞資料 物件和變數C++執行緒函式物件變數
- 如何優雅的使用和理解執行緒池執行緒
- 如何優雅的關閉Java執行緒池Java執行緒
- 程式執行緒、同步非同步、阻塞非阻塞、併發並行執行緒非同步並行
- 如何實現線上優雅停機和調整執行緒池引數?執行緒
- 千萬不要把Request傳遞到非同步執行緒裡面!有坑!非同步執行緒
- c++實現程式與執行緒的同步互斥C++執行緒
- Java 執行緒與同步的效能優化Java執行緒優化
- java中如何給多執行緒中子執行緒傳遞引數?Java執行緒
- 對執行緒、協程和同步非同步、阻塞非阻塞的理解執行緒非同步
- Spring Boot @Async 非同步任務執行Spring Boot非同步
- c++11 執行緒間同步---利用std::condition_variable實現C++執行緒
- 執行緒的同步執行緒
- Core Data:多執行緒大量資料同步執行緒
- C# 多執行緒引數傳遞C#執行緒
- 那些年搞不懂的多執行緒、同步非同步及阻塞和非阻塞(一)---多執行緒簡介執行緒非同步
- Spring Boot(六):如何優雅的使用 MybatisSpring BootMyBatis