作为一个Java开发线程池的使用是一个绕不过去的坎,如何正确的使用线程池是每个开发面临的问题,今天我们就从开源项目中来看看那些顶级开源项目中是如何使用线程池。下面我们就以笔者最近参与的开源项目RocketMQ为例子同时结合笔者在工作中遇到的一些使用一些不好的习惯来说一下线程池使用。会从一下几个方面来说:
1. 线程池的创建
首先我们看一下RocketMQ的线程创建方式,以 BrokerController#initializeResources
方法为例子。如下图:
初看一下好像是自定义的线程池,查看源码就会发现 BrokerFixedThreadPoolExecutor
其实是实现了 ThreadPoolExecutor
本质上还是 ThreadPoolExecutor
,只是进行了语义化。分析上面的线程池创建会发现有以下几个特点:
-
线程池都会设置名称(推荐设置)
为什么需要设置名称?最主要的原因就是在排查问题的时候能够知道是哪个线程池执行的什么代码片段出了问题。如果设置名称使用ThreadPoolExecutor默认的名称会在排查问题的时候不知道是哪个线程池。
-
设置线程池线程数量
这里会根据核心线程数以及最大线程数来判断这个线程池的线程是否需要扩容,例如核心线程数10,最大线程池数100.那么线程池就是在运行过程中超过了核心线程数后就会继续创建线程来满足任务执行。看RocketMQ中的核心线程池和最大线程池一般设置成一样大,也就是固定大小的线程池。
-
自定义任务队列的设置容量(推荐设置)
在RocketMQ中很多都是使用的
LinkedBlockingQueue
,然后给队列设置容量。重点:队列选择根据个人的需求进行选择,但是一定要给队列设置一个合理的容量。如果不设置容量那么默认的容量就是Integer.MAX_VALUE
,这可能很大程度上会导致在触发任务拒绝策略之前内存已经耗光了导致服务宕机。 -
任务拒绝策略选择
任务拒绝策略选择一般情况下可以使用JDK ThreadPoolExecutor的默认策略,如果有特殊的需求用户可以自定义策略。
上面是从RocketMQ的开源项目看到的线程池的创建。其实很多人会发现很少用 Executors
创建。想要详细了解可以看一下 《为什么不建议使用Executors创建线程池分析》 这篇文章。笔者在这篇文章给出了分析。
2. 线程池的使用
研究过RocketMQ源码的人就会发现,RocketMQ有许许多多的线程池,很多人肯定会想问为什么不用一个线程池完成所有的任务。笔者任务主要有以下几个原因。
2.1 单一性原则
单一性原则怎么理解?就是一个线程池应该是做一类任务处理。从 BrokerController#initializeResources
中的代码可以看出来不同的任务使用不同的线程池来处理。这样做的好处:
- 方便回溯线程执行问题,在线程池执行发生错误的时候,这个时候就需要根据错误堆栈进行追踪单一性就方便回溯。如果所有的任务都使用一个线程池去执行,通过线程池名称你是不知道是哪个任务有问题。
- 能够更好的评估队列的大小
- 线程池执行过程中出问题导致线程池shutdown或者其他问题只会影响到一类任务不会影响到全部任务
2.2 适当的封装
对线程池进行适当的封装能够在满足安全性的同时还能让代码看起来更加的优雅,也就是上文提到的语义化
3. 线程池错误使用
在笔者之前的文章中 《Java线程池使用不当会发生什么-生产案例》 给过一个公司生产案例,就是在Controller调用的方法中创建线程池然后去执行异步任务。在这种情况下会出现什么情况就是每次都会创建一个线程池然后这个线程池还不会被销毁,所以每调用一次方法就会创建一个至少一个线程。如果调用1000次方法就会导致有1000个线程被创建。
@Service
@Slf4j
public class AccountAuthServiceImpl implements AccountAuthService {
//省略了部分代码
//功能:将两个系统账号进行绑定
@Override
public boolean bindUser(Long hrsAccountId, Long hmcAccountId){
ExecutorService executorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()*2);
Future<Boolean> hrsExt = executorService.submit(new Callable<Boolean>() {
@Override
public Boolean call() throws Exception {
//查询hrsAccountId是否存在
return true;
}
});
Future<Boolean> hmcExt = executorService.submit(new Callable<Boolean>() {
@Override
public Boolean call() throws Exception {
//查询hmcAccountId是否存在
return true;
}
});
try {
return hrsExt.get(3, TimeUnit.SECONDS) & hmcExt.get(3, TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
}
return false;
}
}
复制代码
4. 总结
- 线程池和线程一定要设置开发者熟悉的名称,方便出现问题时候的排查,这个在其他项目和Java中通过命令都可以发现,线程命名是很重要的一个步骤。虽然Java提供了默认的名字,但是在排查问题中自定义名称显得尤为重要
- 线程池的任务队列容量一定要设置,否则可能会导致耗光内存导致服务宕机
- 线程池的创建不能放在业务方法中创建