线程池中使用ThreadLocal方案


声明:本文转载自https://my.oschina.net/dxqr/blog/2981069,转载目的在于传递更多信息,仅供学习交流之用。如有侵权行为,请联系我,我会及时删除。

人工手打,翻译自: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. 实现原理如上述3所说,这篇翻译的文章中也是该原理,ali提供了一个transmittable-thread-local,原理也是上面3所讲的,不过个人觉得它实现有点绕,用起来还算简单,可以用下

关于threadlocal的代码细节,见我的另外一篇文章:再看ThreadLocal

本文发表于2018年12月08日 08:01
(c)注:本文转载自https://my.oschina.net/dxqr/blog/2981069,转载目的在于传递更多信息,并不代表本网赞同其观点和对其真实性负责。如有侵权行为,请联系我们,我们会及时删除.

阅读 2137 讨论 0 喜欢 0

抢先体验

扫码体验
趣味小程序
文字表情生成器

闪念胶囊

你要过得好哇,这样我才能恨你啊,你要是过得不好,我都不知道该恨你还是拥抱你啊。

直抵黄龙府,与诸君痛饮尔。

那时陪伴我的人啊,你们如今在何方。

不出意外的话,我们再也不会见了,祝你前程似锦。

这世界真好,吃野东西也要留出这条命来看看

快捷链接
网站地图
提交友链
Copyright © 2016 - 2021 Cion.
All Rights Reserved.
京ICP备2021004668号-1