您现在的位置是:首页 > 文章详情

线程池中使用ThreadLocal方案

日期:2018-12-07点击:602

人工手打,翻译自: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

原文链接:https://my.oschina.net/dxqr/blog/2981069
关注公众号

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。

持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。

转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。

文章评论

共有0条评论来说两句吧...

文章二维码

扫描即可查看该文章

点击排行

推荐阅读

最新文章