Spring異步線程池—傳遞線程上下文(TaskDecorator實現)

問題

在spring中使用@async異步調用的情況下,被調用的異步子線程獲取不到父線程的request信息,以便處理相關邏輯,即子線程無法獲取父線程的上下文數據

思路

在自定義的異步線程池ThreadPoolTaskExecutor中,初始化線程池時有taskDecorator這樣一個任務裝飾器,類似aop,可對線程執行方法的始末進行增強。其初始化源碼如下

 protected ExecutorService initializeExecutor(ThreadFactory threadFactory, RejectedExecutionHandler rejectedExecutionHandler) {
        BlockingQueue<Runnable> queue = this.createQueue(this.queueCapacity);
        ThreadPoolExecutor executor;
        if (this.taskDecorator != null) {
            executor = new ThreadPoolExecutor(this.corePoolSize, this.maxPoolSize, (long)this.keepAliveSeconds, TimeUnit.SECONDS, queue, threadFactory, rejectedExecutionHandler) {
                public void execute(Runnable command) {
                    Runnable decorated = ThreadPoolTaskExecutor.this.taskDecorator.decorate(command);
                    if (decorated != command) {
                        ThreadPoolTaskExecutor.this.decoratedTaskMap.put(decorated, command);
                    }

                    super.execute(decorated);
                }
            };
        } else {
            executor = new ThreadPoolExecutor(this.corePoolSize, this.maxPoolSize, (long)this.keepAliveSeconds, TimeUnit.SECONDS, queue, threadFactory, rejectedExecutionHandler);
        }

        if (this.allowCoreThreadTimeOut) {
            executor.allowCoreThreadTimeOut(true);
        }

        this.threadPoolExecutor = executor;
        return executor;
    }

基本使用,自定義裝飾器實現TaskDecorator ,重寫decorate方法,自定義線程池,并設置自定義裝飾器

自定義異步線程池
@Bean("taskExecutor") // bean 的名稱,默認為首字母小寫的方法名
  public Executor taskExecutor() {
    ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
    //其他參數省略
    //設置裝飾器
       threadPoolTaskExecutor.setTaskDecorator(new ContextCopyingDecorator());
    return executor;
  }

自定義裝飾器,ContextCopyingDecorator,通過try,finally,在子線程執行完后將該線程設置的上下文變量清除

public class ContextCopyingDecorator implements TaskDecorator {
    @Override
    public Runnable decorate(Runnable runnable) {
        try {
            //獲取父線程的context
            RequestAttributes context = RequestContextHolder.currentRequestAttributes();
            return () -> {
                try {
                    //將父線程的context設置進子線程里
                    RequestContextHolder.setRequestAttributes(context);
                    //子線程方法執行
                    runnable.run();
                } finally {
                    //清除子線程context
                    RequestContextHolder.resetRequestAttributes();
                }
            };
        } catch (IllegalStateException e) {
            return runnable;
        }
    }
}

存在問題

從父線程取出的RequestContextHolder對象,此為持有線程上下文的request容器,將其設置到子線程中,按道理只要對象還存在強引用,就不會被銷毀,但由于RequestContextHolder的特殊性,在父線程銷毀的時候,會觸發里面的resetRequestAttributes方法(即清除threadLocal里面的信息,即reques中的信息會被清除),此時即使RequestContextHolder這個對象還是存在,子線程也無法繼續使用它獲取request中的數據了。這也是網上很多文章講TaskDecorator時沒提到的點,真正用起來會發現有時可以有時不行,這個就取決于父子線程哪個先結束了。

完善思路

既然是RequestContextHolder的特殊性,那我們就讓繞過他的銷毀清除,思路不變,還是繼續使用threadLocal來傳遞我們需要使用到的變量,在父線程裝飾前將所需變量取出來,然后在子線程中設置到threadLocal,業務使用的時候從threadLocal中取即可。

改造,自定義threadLocal類(此例子以ua為例子),修改自定義裝飾器邏輯

public class ContextCopyingDecorator implements TaskDecorator {
    @Override
    public Runnable decorate(Runnable runnable) {
        try {
            //獲取父線程的request的user-agent(示例)
           HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
            String ua = request.getHeader("user-agent");
            return () -> {
                try {
                    //將父線程的ua設置進子線程里
                    ThreadLocalData.setUa(ua);
                    //子線程方法執行
                    runnable.run();
                } finally {
                    //清除線程threadLocal的值
                    ThreadLocalData.remove();
                }
            };
        } catch (IllegalStateException e) {
            return runnable;
        }
    }
}

ThreadLocalData

public class ThreadLocalData {
    public static final ThreadLocal<String> threadLocal = new ThreadLocal<>();

    public static String getUa(){
        return threadLocal.get();
    }

    public static void setUa(String ua){
        threadLocal.set(ua);
    }

    public static void remove(){
        threadLocal.remove();
    }
}

至此經測試,一切符合預期

涉及知識點

ThreadLocal,InheritableThreadLocal,TaskDecorator,RequestContextHolder,TransmittableThreadLocal(通過繼承InheritableThreadLocal實現,阿里的,推薦)

測試 ThreadLocal,InheritableThreadLocal,TransmittableThreadLocal的區別和使用

1.父線程使用ThreadLocal,子線程創建時不會擁有父類的threadLocal信息
2.父線程使用InheritableThreadLocal,子線程創建時,默認init方法會拿到父類的InheritableThreadLocal信息,這種在線程池/線程復用的情況下,由于init方法只會在初始化時獲取父線程的數據,復用的時候也沒法再從父線程那里新的InheritableThreadLocal的數據,此種情況下繼續使用,很容易出bug(InheritableThreadLocal適用于非線程池和復用線程,單獨創建銷毀子線程執行的情況)
3.父線程使用TransmittableThreadLocal,子線程創建時擁有父類的TransmittableThreadLocal信息,在線程池/線程復用的情況下不會出現讀取到臟數據的情況

總結

  • 在異步線程池的情況下,通過ThreadLocal+TaskDecorator一般即可解決遇到的透傳問題(方式1)
  • 使用阿里的TransmittableThreadLocal,其原理也是對Runnable,Callable,進行裝飾(方式2)

參考

Spring線程池—TaskDecorator線程的裝飾(跨線程傳遞ThreadLocal的方案)
(28條消息) TaskDecorator——異步多線程中傳遞上下文等變量_WannaRunning的博客-CSDN博客

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容