java线程池threadlocal_线程池中使用ThreadLocal方案

尊重外国人写文章的习惯,如果你初次看到此类翻译可能会造成不愉悦,但如果你曾经看到过,那你一定明白我在说什么,有的地方加上我自己的理解和注释

在这篇文章里,我们将会演示如何从web线程里复制MDC数据到@Async注解的线程里,我们将会使用一个全新的 Spring Framework 4.3的特性: ThreadPoolTaskExecutor#setTaskDecorator() [set-task-decorator]. 下面是最终结果:

09747d7dcfe55dfd448470ab13d14c2a.png

注意到倒数第二行和第三行:在这个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 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> 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 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:(这一句怎么翻译?)

参考

以下自己的总结:

使用ThreadLocal,不会在子线程中(包括new Thread和new线程池)获取到

使用InheritableThreadLocal,可以在子线程中(包括new Thread和new线程池)获取到,但是如果用的是线程池,一般不会每次使用的时候重新创建,而他的赋值只能在首次创建的时候可以(Thread类的inheritableThreadLocals变量),后面线程池中的线程重复使用时,一开始赋值的那个变量将会一直存在,你可能会得到错误的结果或者理解为这也是一种内存泄漏

在spring中,一般通过xml或者@Configuration来配置线程池,那么在项目启动的时候,线程池就完成创建了,根本没有机会给你设置变量,所以最佳实践就是,在线程池提交任务的时候(execute和submit方法),把当前线程的threadlocal变量保存起来,重写run方法或者call方法,并且在调用实际的run方法前,保存刚才保存起来的变量,一般也是放到threadlocal里面,这样在实际的run方法里,就可以方便的通过threadlocal获取到了。

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

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

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值