为什么需要线程池参数动态化?
日常业务开发中,线程池参数是很难计算准确的,往往需要在实践中不断的调整才能得到一个较为合理的取值。在取值不是那么明确之前,每次修改参数都需要重新部署服务才能生效,这显然不是一个优雅的实践方案,所以我们需要将线程池参数动态化,一旦线程池发生故障,可以快速调整参数且能立即生效,有效缩短故障时间。
代码实践
代码基于 Springboot 2.7.0
- 先配置一个ThreadPoolTaskExecutor实例,初始核心线程数为1,最大线程数为2,队列容量为5。
@Bean("myExecutor")
public ThreadPoolTaskExecutor asyncExecutorService() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(1);
executor.setMaxPoolSize(2);
executor.setQueueCapacity(5);
executor.setKeepAliveSeconds(10);
executor.setThreadNamePrefix("My-Executor-");
executor.initialize();
return executor;
}
- 创建两个api,/business 用于模拟业务请求,/thread-pool/param 用于动态修改线程池参数。
@Slf4j
@RestController
public class MyController {
@Autowired
private ThreadPoolTaskExecutor myExecutor;
@PostMapping("/business")
public void newBusiness() throws InterruptedException {
for (int i = 0; i < 10; i++) {
final int index = i + 1;
try {
myExecutor.execute(() -> {
try {
Thread.sleep(3_1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
log.info("This is my business{}", index);
});
}catch (RejectedExecutionException e){
log.error("Business{} was rejected", index);
}
}
}
@PutMapping("/thread-pool/param")
public void updateThreadPool(@RequestBody UpdateThreadPoolParam updateData) {
log.info("corePoolSize before => {}", myExecutor.getCorePoolSize());
log.info("maxPoolSize before => {}", myExecutor.getMaxPoolSize());
log.info("keepAliveSeconds before => {}", myExecutor.getKeepAliveSeconds());
myExecutor.setCorePoolSize(updateData.getCorePoolSize());
myExecutor.setMaxPoolSize(updateData.getMaxPoolSize());
myExecutor.setKeepAliveSeconds(updateData.getKeepAliveSeconds());
log.info("corePoolSize after => {}", myExecutor.getCorePoolSize());
log.info("maxPoolSize after => {}", myExecutor.getMaxPoolSize());
log.info("keepAliveSeconds after => {}", myExecutor.getKeepAliveSeconds());
}
}
- 执行请求(curl -X POST /business)
我们发起了10个任务,因为最大线程数是2,队里容量为5,初始线程池最多能同时持有7个任务,2个执行中的任务和5个队列中等待的任务,所以最终结果就是最后3个任务被拒绝。
- 执行请求 (curl -H “Content-Type:application/json” -X PUT -d ‘{“corePoolSize”:3, “maxPoolSize”:10, “keepAliveSeconds”:30}’ /thread-pool/param) 修改参数
- 核心线程数 = 3
- 最大线程数 = 10
- 线程空闲等待时间 = 30s
可以看到,线程池参数已经修改成功。
5. 再次执行请求(curl -X POST /business) 验证修改后的参数
同样是发起10个任务,由运行结果可知,10个任务共由5个线程一起处理。因为队列容量为5,所以只需要5个线程(3个核心线程 + 2个非核心线程)即可。
所以目前线程池参数已经不是最初的值了(核心线程为1,最大线程为2),验证通过。
注:构造线程池时有8个参数,但是最核心的是3个:核心线程数、最大线程数,工作队列。
本实践中只实现了前两个参数的动态修改,因为JDK自带的ThreadPoolExecutor并没有对外提供设置workQueue及其容量大小的方法。虽然spring提供的ThreadPoolTaskExecutor可以设置队列容量,但其只在初始化之前生效,一旦初始化完成再去修改这个值就是无效的。如 ThreadPoolTaskExecutor源码所示:
// 设置核心线程数时会同步更新threadPoolExecutor中的核心线程数
public void setCorePoolSize(int corePoolSize) {
synchronized (this.poolSizeMonitor) {
if (this.threadPoolExecutor != null) {
this.threadPoolExecutor.setCorePoolSize(corePoolSize);
}
this.corePoolSize = corePoolSize;
}
}
// 而设置队列容量的时候只更新其本身的属性值
public void setQueueCapacity(int queueCapacity) {
this.queueCapacity = queueCapacity;
}
如果确实要实现动态调整工作队列,我们可以通过ThreadPoolExecutor提供的getQueue()方法获取到工作队列对象实例然后修改其容量,但是默认BlockingQueue一旦创建就没法修改其容量大小,所以我们需要自己实现一个可以动态调整容量大小的队列即可。