Java 线程池详解¶
Java 线程池的本质:提前创建并复用一组线程,用队列缓存任务,用一套“线程数 + 队列 + 拒绝策略”的规则来管理任务提交与执行。
为什么要用线程池¶
- 降低线程创建/销毁开销:线程是重量级资源,频繁 new Thread 成本高,线程池通过复用已有线程减少开销。
- 提高响应速度:任务来了直接丢给已有线程,无需等待创建线程。
- 可控的资源管理:统一控制并发线程数、队列大小和拒绝策略,避免“线程无限建、资源打满”。
- 便于监控与调优:线程池暴露活跃线程数、队列长度、已完成任务数等指标,方便定位性能瓶颈。
线程池核心参数(ThreadPoolExecutor)¶
常用构造函数:
public ThreadPoolExecutor(
int corePoolSize, // 核心线程数
int maximumPoolSize, // 最大线程数
long keepAliveTime, // 非核心线程空闲存活时间
TimeUnit unit, // keepAliveTime 时间单位
BlockingQueue<Runnable> workQueue, // 任务队列
ThreadFactory threadFactory, // 线程工厂
RejectedExecutionHandler handler // 拒绝策略
)
关键参数含义(记住这几个就够用):
- corePoolSize:核心线程数,线程池“常驻工人”数量,正常情况下优先用核心线程执行任务。
- maximumPoolSize:线程池允许的最大线程数 = 核心线程 + 非核心线程上限。
- keepAliveTime + unit:非核心线程空闲超过这个时间就会被回收;有配置时也可让核心线程参与回收。
- workQueue:保存等待执行任务的阻塞队列(如 ArrayBlockingQueue、LinkedBlockingQueue 等)。
- threadFactory:创建线程的工厂,常用来给线程命名、设置守护线程、统一异常处理。
- handler:拒绝策略,在线程池和队列都满了之后决定如何处理新任务(丢弃、抛异常、调用方执行等)。
推荐做法:生产环境使用 ThreadPoolExecutor 构造函数手动创建线程池,不直接用 Executors 提供的快捷方法(容易导致队列无界、OOM 等隐患)。
线程池的工作流程(任务提交流程)¶
线程池内部就是一个“生产者-消费者 + 线程复用”模型:外部提交任务,线程池判断是交给线程执行、入队还是拒绝。
当调用 execute(Runnable command) 时,大致流程是:
- 如果当前运行线程数 < corePoolSize:
- 直接创建新的核心线程来执行任务,不进队列。
- 否则,如果线程数 ≥ corePoolSize:
- 尝试把任务放入 workQueue 等待执行(如果队列没满)。
- 如果队列满了:
- 且当前线程数 < maximumPoolSize:创建非核心线程执行任务。
- 如果队列满 & 线程数 ≥ maximumPoolSize:
- 触发拒绝策略 handler(抛异常/丢弃任务/调用方线程执行等)。
示例图景:
- corePoolSize=2,maximumPoolSize=4,队列容量=10。
- 前 2 个任务来:立刻开启 2 个核心线程执行。
- 第 3~12 个任务:进入队列等待。
- 第 13、14 个任务:创建非核心线程到 4 个。
- 第 15 个任务:线程池+队列满,触发拒绝策略。
常见线程池类型(Executors 工具类)¶
Executors 只是帮你包装不同参数组合,本质还是 ThreadPoolExecutor。
| 线程池类型 | 创建方式 | 特点与风险简述 |
|---|---|---|
| 固定大小线程池 | Executors.newFixedThreadPool(n) | 核心线程 = 最大线程 = n,使用无界队列,任务过多易 OOM。javaguide+1 |
| 缓存线程池 | Executors.newCachedThreadPool() | 线程数可扩到 Integer.MAX_VALUE,短任务多时适用,极端下也易撑爆。javaguide+1 |
| 单线程线程池 | Executors.newSingleThreadExecutor() | 永远一个线程,串行执行任务,适合顺序任务但仍有无界队列风险。javaguide+1 |
| 定时任务线程池 | Executors.newScheduledThreadPool(n) | 支持延时/周期任务,底层是 ScheduledThreadPoolExecutor。javaguide |
生产环境尽量不用上面这些直接工厂方法,而是自己 new ThreadPoolExecutor,设置有界队列并明确拒绝策略。
线程池核心参数如何配置(经验规则)¶
常用经验规则(实际要结合压测):
- CPU 密集型任务(主要是计算):
- 线程数 ≈ CPU 核心数(或略多一点,如 N 或 N+1)。
- I/O 密集型任务(大量网络/磁盘等待):
- 线程数 ≈ N × M,N 为 CPU 核心数,M 通常取 2~4,根据“等待时间/计算时间”比例调优。
- 更严谨的公式:
- 最佳线程数 ≈ N × (1 + WT / ST),N 为核心数,WT 是等待时间,ST 是计算时间。 典型 Spring 配置示例(伪代码):
@Configuration
@EnableAsync
public class ThreadPoolExecutorConfig {
@Bean("bizThreadPool")
public Executor threadPoolExecutor() {
int processors = Runtime.getRuntime().availableProcessors();
ThreadPoolExecutor executor = new ThreadPoolExecutor(
processors, // corePoolSize
processors * 2, // maximumPoolSize
60L, TimeUnit.SECONDS, // keepAliveTime
new LinkedBlockingQueue<>(1000), // 有界队列
new ThreadFactory() { // 命名线程
private final AtomicInteger idx = new AtomicInteger(1);
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r);
t.setName("biz-pool-" + idx.getAndIncrement());
return t;
}
},
new ThreadPoolExecutor.AbortPolicy() // 拒绝策略
);
return executor;
}
}
关键语句说明:
- LinkedBlockingQueue<>(1000):限制队列长度,避免无限堆积任务。
- 自定义 ThreadFactory:给线程命名,排查问题时一眼能看出是哪个线程池。
- AbortPolicy:拒绝时抛异常,提醒你线程池已经顶不住了,方便报警与扩容。
线程池中的拒绝策略¶
JDK 自带四种常用策略(RejectedExecutionHandler 实现):
- AbortPolicy:直接抛 RejectedExecutionException(默认)。
- CallerRunsPolicy:由提交任务的线程自己执行任务(降低新任务并发度)。
- DiscardOldestPolicy:丢弃队列中最旧的任务,再尝试提交当前任务。
- DiscardPolicy:静默丢弃当前任务。
实际项目中常用:AbortPolicy(+监控报警)或 CallerRunsPolicy(削峰)。也可以自定义策略做日志、降级等。
线程池的监控与常见坑¶
常见最佳实践与坑位:
- 最佳实践:
- 统一封装线程池创建,不在业务代码到处 new。
- 监控:定期打印或上报线程池状态(poolSize、activeCount、queueSize、completedTaskCount)。
- 为不同业务使用不同线程池(IO、计算、RPC 调用等),避免互相拖垮。
- 典型坑:
- 父任务在同一个线程池提交子任务并 get() 等待,而核心线程被父任务占满,子任务排队没线程可用,造成伪“死锁”。
- 使用无界队列但 maximumPoolSize 设置很大,实际永远到不了扩容逻辑,队列无限堆积。
- 关闭忘记调用 shutdown() / shutdownNow() 导致程序无法优雅退出。
简单监控示例(定时打印状态):
public static void printThreadPoolStatus(ThreadPoolExecutor threadPool) {
ScheduledExecutorService monitor = new ScheduledThreadPoolExecutor(1);
monitor.scheduleAtFixedRate(() -> {
System.out.println("============");
System.out.println("PoolSize: " + threadPool.getPoolSize());
System.out.println("ActiveCount: " + threadPool.getActiveCount());
System.out.println("CompletedTask: " + threadPool.getCompletedTaskCount());
System.out.println("QueueSize: " + threadPool.getQueue().size());
System.out.println("============");
}, 0, 1, TimeUnit.SECONDS);
}