1. 程式人生 > >執行緒池中使用ThreadLocal方案

執行緒池中使用ThreadLocal方案

人工手打,翻譯自:https://moelholm.com/2017/07/24/spring-4-3-using-a-taskdecorator-to-copy-mdc-data-to-async-threads 本來想自己寫一篇關於執行緒池threadlocal的,偶然看到這篇文章覺得挺好的,便直接翻譯了

尊重外國人寫文章的習慣,如果你初次看到此類翻譯可能會造成不愉悅,但如果你曾經看到過,那你一定明白我在說什麼,有的地方加上我自己的理解和註釋


在這篇文章裡,我們將會演示如何從web執行緒裡複製MDC資料到@Async註解的執行緒裡,我們將會使用一個全新的 Spring Framework 4.3的特性: ThreadPoolTaskExecutor#setTaskDecorator() [set-task-decorator]. 下面是最終結果:

注意到倒數第二行和第三行:在這個log級別上輸出了[userId:Duke],倒數第三行是在一個web執行緒裡(一個使用@RestController註解的類)發出的,倒數第二行是在一個用了@Async註解的非同步執行緒裡發出的。本質上,MDC資料從web執行緒中複製到了使用@Async註解的非同步執行緒裡中了(這就是最酷的部分,:smirk:)
繼續閱讀吧,少年,去看看這是怎麼實現的。這篇文章的所有程式碼都可以在GitGub上的示例中找到。如果有需要的話,可以去看看細節。

關於示例專案

這個示例專案基於Spring Boot 2。日誌API這裡用的是SLF4J和Logback(用了Logger, LoggerFactory和MDC) 如果你去看了那個示例專案,你將會發現這個@RestController註解的Controler

@RestController
public class MessageRestController {

  private final Logger logger = LoggerFactory.getLogger(getClass());

  private final MessageRepository messageRepository;

  MessageRestController(MessageRepository messageRepository) {
    this.messageRepository = messageRepository;
  }

  @GetMapping
  List<String> list() throws Exception {
    logger.info("RestController in action");
    return messageRepository.findAll().get();
  }
}

注意到它輸出了日誌:RestController in action,同時注意到它有一個古怪的呼叫:messageRepository.findAll().get(),這是因為它執行了一個非同步的方法,接收了一個Future物件,並且呼叫了get()方法來等待結果返回,所以這是一個在web執行緒裡呼叫使用@Async註解的非同步方法。這是一個很顯然的人為的為了演示而寫的示例(我猜你在工作中的一些場景中會明智的呼叫此類非同步方法)
下面是那個repository類:

@Repository
class MessageRepository {

  private final Logger logger = LoggerFactory.getLogger(getClass());

  @Async
  Future<List<String>> findAll() {
    logger.info("Repository in action");
    return new AsyncResult<>(Arrays.asList("Hello World", "Spring Boot is awesome"));
  }
}

注意到findAll方法裡列印了日誌:Repository in action。
為了完整起見,讓我向你展示如何在web執行緒裡設定MDC資料的:

@Component
public class MdcFilter extends GenericFilterBean {

  @Override
  public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
      throws IOException, ServletException {
    try {
      MDC.put("mdcData", "[userId:Duke]");
      chain.doFilter(request, response);
    } finally {
      MDC.clear();
    }
  }
}

如果我們什麼也不做,我們可以在web執行緒裡很輕鬆的拿到正確配置的MDC資料,但是當一個web請求進入了@Async註解的非同步方法呼叫裡,我們卻不能跟蹤它:MDC資料裡的ThreadLocal資料不會簡單的自動複製過來,好訊息是這個超級簡單解決

解決方案第一步: 配置@Async執行緒池

首先,定製化你的非同步功能,我是這樣做的:

@EnableAsync(proxyTargetClass = true)
@SpringBootApplication
public class Application extends AsyncConfigurerSupport {

  @Override
  public Executor getAsyncExecutor() {
    ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
    executor.setTaskDecorator(new MdcTaskDecorator());
    executor.initialize();
    return executor;
  }

  public static void main(String[] args) {
    SpringApplication.run(Application.class, args);
  }
}

有意思的地方是我們擴充套件了AsyncConfigurerSupport,好讓我們可以自定義執行緒池
更精確的說:祕密在於executor.setTaskDecorator(new MdcTaskDecorator())。就是這行程式碼使我們可以自定義TaskDecorator

解決方案第二步: 實現TaskDecorator

現在到了說明自定義的TaskDecorator:

class MdcTaskDecorator implements TaskDecorator {

  @Override
  public Runnable decorate(Runnable runnable) {
    // Right now: Web thread context !
    // (Grab the current thread MDC data)
    Map<String, String> contextMap = MDC.getCopyOfContextMap();
    return () -> {
      try {
        // Right now: @Async thread context !
        // (Restore the Web thread context's MDC data)
        MDC.setContextMap(contextMap);
        runnable.run();
      } finally {
        MDC.clear();
      }
    };
  }
}

decorate()方法的引數是一個Runnable物件,返回結果也是另一個Runnable物件
這裡,我只是把原始的Runnable物件包裝了一下,首先取得MDC資料,然後把它放到了委託的run方法裡(Here, I basically wrap the original Runnable and maintain the MDC data around a delegation to its run() method.英文原文是這樣,太難翻譯了,囧)

總結

從web執行緒裡複製MDC資料到非同步執行緒是如此的容易,這裡展示的技巧不侷限於複製MDC資料,你也可以使用它來複制其他ThreadLocal資料(MDC內部就是使用ThreadLocal),或者你可以使用TaskDecorator做一些其他完全不同的事情:記錄日誌,度量方法執行的時間,吞掉異常,退出JVM等等,只要你喜歡

牆裂感謝Joris Kuipers (@jkuipers)提醒我這個牛逼的Spring Framework 4.3新功能, An awesome tip :hugging:(這一句怎麼翻譯?).

參考

[set-task-decorator] ThreadPoolTaskExecutor#setTaskDecorator() (Spring’s JavaDoc) https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/scheduling/concurrent/ThreadPoolTaskExecutor.html#setTaskDecorator-org.springframework.core.task.TaskDecorator-


以下自己的總結:

  1. 使用ThreadLocal,不會在子執行緒中(包括new Thread和new執行緒池)獲取到
  2. 使用InheritableThreadLocal,可以在子執行緒中(包括new Thread和new執行緒池)獲取到,但是如果用的是執行緒池,一般不會每次使用的時候重新建立,而他的賦值只能在首次建立的時候可以(Thread類的inheritableThreadLocals變數),後面執行緒池中的執行緒重複使用時,一開始賦值的那個變數將會一直存在,你可能會得到錯誤的結果或者理解為這也是一種記憶體洩漏
  3. 在spring中,一般通過xml或者@Configuration來配置執行緒池,那麼在專案啟動的時候,執行緒池就完成建立了,根本沒有機會給你設定變數,所以最佳實踐就是,寫一個代理類,線上程池提交任務的時候(execute和submit方法),把當前執行緒的threadlocal變數儲存起來,重寫run方法或者call方法,並且在呼叫實際的run方法前,儲存在剛才儲存起來的變數,一般也是放到threadlocal裡面,這樣在實際的run方法裡,就可以方便的通過threadlocal獲取到了
  4. 除了上述的方法,ali提供了一個transmittable-thread-local,原理就是上面3所講的,不過個人覺得它實現有點繞,用起來還算簡單,可以用下

關於threadlocal的程式碼細節,見我的另外一篇文章:再看ThreadLocal