線程池中使用ThreadLocal方案

人工手打,翻譯自:https://moelholm.com/2017/07/24/spring-4-3-using-a-taskdecorator-to-copy-mdc-data-to-async-threads 原本想本身寫一篇關於線程池threadlocal的,偶然看到這篇文章以爲挺好的,便直接翻譯了html

尊重外國人寫文章的習慣,若是你初次看到此類翻譯可能會形成不愉悅,但若是你曾經看到過,那你必定明白我在說什麼,有的地方加上我本身的理解和註釋java


在這篇文章裏,咱們將會演示如何從web線程裏複製MDC數據到@Async註解的線程裏,咱們將會使用一個全新的 Spring Framework 4.3的特性: ThreadPoolTaskExecutor#setTaskDecorator() [set-task-decorator]. 下面是最終結果:
git

注意到倒數第二行和第三行:在這個log級別上輸出了[userId:Duke],倒數第三行是在一個web線程裏(一個使用@RestController註解的類)發出的,倒數第二行是在一個用了@Async註解的異步線程裏發出的。本質上,MDC數據從web線程中複製到了使用@Async註解的異步線程裏中了(這就是最酷的部分,:smirk:)
繼續閱讀吧,少年,去看看這是怎麼實現的。這篇文章的全部代碼均可以在GitGub上的示例中找到。若是有須要的話,能夠去看看細節。github

關於示例項目

這個示例項目基於Spring Boot 2。日誌API這裏用的是SLF4J和Logback(用了Logger, LoggerFactory和MDC) 若是你去看了那個示例項目,你將會發現這個@RestController註解的Controlerweb

@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類:spring

@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數據的:api

@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數據不會簡單的自動複製過來,好消息是這個超級簡單解決app

解決方案第一步: 配置@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())。就是這行代碼使咱們能夠自定義TaskDecoratorasync

解決方案第二步: 實現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. 實現原理如上述3所說,這篇翻譯的文章中也是該原理,ali提供了一個transmittable-thread-local,原理也是上面3所講的,不過我的以爲它實現有點繞,用起來還算簡單,能夠用下

關於threadlocal的代碼細節,見個人另一篇文章:再看ThreadLocal

相關文章
相關標籤/搜索