本文首发于公众号:托尼学长,立个写 1024 篇原创技术面试文章的flag,欢迎过来视察监督~
这是Java方向的一个非常常见的面试题,对于该题的理解有两个流派。
流派一
先将系统进行类型区分,到底属于CPU密集型还是IO密集型。
-
CPU密集型的任务:也就是需要进行复杂计算的业务场景,线程数应接近CPU核心数,避免过多的线程切换损耗性能,通常建议设置为 CPU核心数 +1。
-
I/O密集型任务:那些以数据库CRUD操作为主的业务系统,线程数可设置为 CPU核心数 ×2,充分利用CPU在I/O等待时的空闲时间。
代码示例如下:
// CPU密集型任务(8核服务器)
ThreadPoolExecutor cpuPool = new ThreadPoolExecutor(
8 + 1, // 核心线程数
8 + 1, // 最大线程数
60, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(1000) // 队列容量需根据任务量设置
);
// I/O密集型任务(8核服务器)
ThreadPoolExecutor ioPool = new ThreadPoolExecutor(
8 * 2, // 核心线程数
8 * 2,
60, TimeUnit.SECONDS,
new SynchronousQueue<>() // 无界队列可能导致内存溢出,需谨慎使用
);
流派二
这种流派的鼻祖,来自于《Java 并发编程实战》一书,提出以公式的方式来计算线程数量。
计算公式为:线程数 = CPU 核数 × CPU 利用率 × (1 + 等待时间/计算时间)
接下来,我们将公式中的这些元素进行一一拆解。
CPU核数:指的是获取当前系统可用的处理器数量,可通过Java代码中的Runtime.getRuntime().availableProcessors()进行获取。
CPU利用率:通常设为 0.8–0.9,避免资源耗尽。
等待时间/计算时间: 所执行任务中 I/O 等待、网络延迟等非计算耗时与 CPU 计算耗时的比值。
假设8核CPU,CPU利用率为0.8,任务计算耗时为50ms,等待耗时为200ms,那线程数量的最优解为:8 * 0.8 * (1 + 200/50)= 32。
真实场景
上述这两种在线程池中设置线程的方式,被很多同学奉为神明,不仅在技术面试中这样回答,而且在真实业务系统中也是这样设置的。
但在真实的业务系统中,真的能这么简简单单地推算出来吗?当然不能。
原因在于,就算现在主流的系统架构都是采用微服务架构进行设计的,那也不可能一个服务中仅仅运行这一个接口或一段业务逻辑吧?
如果系统中的硬件资源被这区区一个线程池给吃干抹净了,那其他的业务逻辑还跑不跑了?
另外,就算昨天设置8个线程为最优解,并不代表今天和以后仍然是最优解,毕竟系统的代码是变化的,系统数据也是动态的。
所以,在真实业务场景中,随时根据当前情况进行阈值调配,才是系统中的最优解,我们可以了解一下动态线程池。
动态线程池
动态线程池是一种可根据系统负载、任务队列状态等实时参数,动态调整线程数量及配置的线程管理机制。
该机制解决了传统线程池因固定参数,从而导致的资源浪费或处理能力不足问题。
目前市面上主流的动态线程池有DynamicTp和Hippo4j,我们就以DynamicTp为例,与静态线程池进行对比。
对比维度 | DynamicTp | 静态线程池 |
---|---|---|
核心参数调整 | 支持运行时动态调整核心线程数、最大线程数、队列容量等参数,通过配置中心(如 Nacos、Apollo)实现热更新。 | 参数(核心线程数、队列容量等)在初始化时固定修改需修改代码并重新部署。 |
资源利用率 | 根据负载自动弹性扩缩容,支持非核心线程超时回收,避免资源浪费或过载。 | 线程数固定,低负载时易闲置,高负载时队列堆积可能导致任务延迟或拒绝。 |
监控与告警 | 内置实时监控(任务拒绝率、活跃线程数等),支持邮件、钉钉等告警通道和 Grafana 可视化。 | 需手动实现监控逻辑,通常缺乏告警能力。 |
适用场景 | 高并发波动场景(如秒杀、大促)、微服务多组件线程池统一管理。 | 任务量稳定且可预测的场景(如定时批处理任务)。 |
扩展性 | 提供 SPI 扩展接口,支持自定义配置中心、监控采集器等组件。 | 无扩展能力,需自行封装或依赖外部工具。 |
维护成本 | 依赖配置中心及监控体系,维护成本较高但长期优化效果显著。 | 实现简单、无外部依赖,但参数调优需反复重启服务,长期成本较高。 |
DynamicTp架构图:
DynamicTp是采用模块分层设计设计的,其中包括:
Adapter 模块,负责适配第三方组件(如 Tomcat、Dubbo、Jetty)的线程池,通过拦截或扩展原生线程池实现统一管理。
Core 模块,核心逻辑层,实现动态参数调整、监控数据采集、告警触发等核心功能,基于 ThreadPoolExecutor
扩展方法实现运行时调参。
Starter 模块,集成不同配置中心(如 Nacos、Apollo),监听配置变化并同步至线程池,提供开箱即用的接入能力。
Logging/监控模块,定时采集线程池指标(任务拒绝率、活跃线程数等),支持通过 MicroMeter、JSON 日志或 SpringBoot Endpoint 输出数据。
代码demo实现如下:
import org.dromara.dynamictp.core.spring.DynamicTp;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.util.concurrent.ThreadPoolExecutor;
@RestController
public class DemoController {
// 通过 @DynamicTp 注解注入线程池(名称与配置中的 threadPoolName 一致)
@Resource
@DynamicTp("demoThreadPool")
private ThreadPoolExecutor demoThreadPool;
// 模拟任务提交接口
@GetMapping("/submit-task")
public String submitTask() {
for (int i = 0; i < 50; i++) {
int taskId = i;
demoThreadPool.execute(() -> {
try {
System.out.println("执行任务-" + taskId + " | 线程: "
+ Thread.currentThread().getName());
Thread.sleep(1000); // 模拟耗时任务
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
return "任务已提交!当前线程池状态:\n" +
"活跃线程数: " + demoThreadPool.getActiveCount() + "\n" +
"队列大小: " + demoThreadPool.getQueue().size();
}
}
所以,作为一个Java技术人,我们一定要与时俱进,哪怕是应对这些常见的八股文面试。