Java中的异步编程&线程池管理

这篇文章主要记录一下关于Java中,或者说Spring Boot中关于线程池和异步执行方法的问题。因为在项目里踩了坑,所以也想记录一下关于这方面的学习过程和思考过程。

起因

先阐述一下本次学习的起因。本身我对于异步编程,或者说多线程编程的认知并不是非常全面,应该说是一个弱项。对于多线程的认知也停留在Java的concurrent包内,实际上手经验几乎为零。接触Spring Boot之后依靠的也只是框架的遍历,自己也并没有认真思考过多线程的问题。

在一次业务开发过程中,出现了跟主业务无关,但是必须需要通知的内容。考虑到效率问题,因此决定将此部分内容抽离出来进行异步调用,以达到减少主线程耗时、减少接口链接压力的目的。抽离出来的内容主要为:

  1. 更新数据
  2. 通知其他服务事件

到此为止整件事情听上去非常完美,但是坑也就埋在这里:公司组件内用于控制当前用户身份信息以及切换分库的数据均存储在ThreadLocal中,因此当异步线程开启时,存储在主线程中ThreadLocal中的内容,在子线程中全部消失,导致用户身份信息获取失败、分库切换的失效。

于是就在开启子线程时,就提前把ThreadLocal中的数据取出,作为参数传递给子线程任务。OK,完美解决。

思考

bug是解决了,但是也牵扯出很多问题来:

  1. 公司组件的经历过多次迭代,初版是可以让ThreadLocal数据在子线程内也生效,为什么这次就不行?
  2. 开启异步任务的方式有很多,ThreadLocal的数据维护是否跟子线程的启用方式有关?
  3. 在当前框架内(Spring Boot)开启异步子线程,是否依靠了线程池?

其实列出上述3个问题的时候,相信大家心里已经有了答案了:没错就是线程池的线程复用。

为什么ThreadLocal无法在异步线程内生效

关于ThreadLocal以及其衍生内容可见:ThreadLocal详解
就像问题1中所描述的,ThreadLocal中维护的数据仅当前线程可见,那么在初版组件中能够被继承到子线程中有两种可能:

  1. 线程创建时,重要数据被重新赋值到异步线程的ThreadLocal中
  2. 使用了可继承的InheritableThreadLocal或者TransmittableThreadLocal

InheritableThreadLocal

该种ThreadLocal会在子线程创建的时候从父线程复制到子线程中,也仅仅只复制InheritableThreadLocal的内容,当子线程销毁时,引用也跟着断开。当然,创建子线程时会赋值其他参数,例如:线程优先级priority、类加载器contextClassLoader,不在本文描述范围内。
由于此种ThreadLocal仅在创建时被继承,因此当使用线程池的时候,就会产生问题:无法继承当前主线程的InheritableThreadLocal内容。

TransmittableThreadLocal

该种ThreadLocal需要配合TtlRunnable一同使用。TtlRunnalbe提供了一个get方法用于封装一个runnable。该封装在执行Runnable前备份了当前的ThreadLocal内容,赋值父线程的ThreadLocal内容,执行完毕后再把备份ThreadLocal内容复制回去。

开启异步任务的方式

就像上一个小节所描述的,如果使用线程池,那么ThreadLocal就会容易出现各种各样的问题。而在标准Spring Boot开发过程中,应当并且推荐使用线程池的方式开启异步任务。
在实践过程中,通常使用到的开启异步任务的方式主要有:

  • CompletableFuture
  • @Async

经过相关资料的查阅,@Async注解最终会调用Executor线程池进行方法调用;CompletableFuture底层也是使用线程池进行方法调用,但是多了许多更灵活的结果处理方法,例如:thenApply、thenApplyAsync、thenAccept、thenAcceptAsync等等。
此处更推荐使用CompletableFuture类以及相关方法,其默认使用ForkJoinPool或者ThreadPerTaskExecutor作为线程池。两者在默认初始化时:

  1. 当处理器核数大于1时,默认使用ForkJoinPool
  2. 当处理器核数小于等于1时,默认使用ThreadPerTaskExecutor,该线程池会为每个任务开设一条线程,相当于没有线程池。

同时,CompletableFuture也支持手动指定执行的线程池。两者视使用场景而定,唯一不变的核心是:线程池。

结论

最终确认当涉及到使用线程池的使用时,会导致ThreadLocal失效。因此在启用异步方法调用时,尽量平整化参数引用,避免ThreadLocal此类操作的强依赖。

参考

https://blog.csdn.net/belongtocode/article/details/139218832

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值