面试官:系统中线程池的线程数量,设置多少比较合理?

69 阅读5分钟

本文首发于公众号:托尼学长,立个写 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技术人,我们一定要与时俱进,哪怕是应对这些常见的八股文面试。