在Java并发编程中,线程池(Thread Pool)是一种基于池化思想管理线程的工具。它通过预先创建好若干个线程放入池中,当有任务提交时,直接从池中取出空闲线程执行,任务结束后线程并不销毁,而是放回池中等待下一次任务。这种机制极大地提高了线程的复用性,降低了资源消耗。
通过重复利用已创建的线程,降低线程创建和销毁造成的损耗。频繁创建线程会消耗大量的CPU时间和内存资源。
当任务到达时,线程可以立即执行,无需等待线程创建。这对于需要快速响应的业务场景至关重要。
线程是稀缺资源,如果无限制创建,不仅会消耗系统资源,还会降低系统的稳定性。线程池可以进行统一的分配、调优和监控。
在早期开发中,我们习惯直接使用 new Thread().start() 来执行任务。然而,这种方式存在显著缺陷:
new Thread 新建对象,性能差。因此,理解 线程池的原理 成为Java开发者进阶的必经之路。
Java中最常用的线程池实现类是 ThreadPoolExecutor。要深入理解其原理,必须掌握其七大核心参数。这七个参数共同决定了线程池的行为模式。
| 参数名称 | 类型 | 说明 |
|---|---|---|
| corePoolSize | int | 核心线程数。线程池中的常驻核心线程数,即使它们处于空闲状态,也不会被回收(除非设置了allowCoreThreadTimeOut)。 |
| maximumPoolSize | int | 最大线程数。线程池能够创建的最大线程数,包括核心线程和非核心线程。 |
| keepAliveTime | long | 存活时间。非核心线程空闲后的存活时间,超过此时间将被回收。 |
| unit | TimeUnit | 时间单位。keepAliveTime的时间单位,如秒、毫秒等。 |
| workQueue | BlockingQueue | 任务队列。用于存储等待执行的任务,常见的有ArrayBlockingQueue、LinkedBlockingQueue等。 |
| threadFactory | ThreadFactory | 线程工厂。用于创建新线程,默认使用Executors.defaultThreadFactory(),可以自定义线程名称、优先级等。 |
| handler | RejectedExecutionHandler | 拒绝策略。当队列和线程池都满了,说明线程池处于饱和状态,必须采取一种策略处理新提交的任务。 |
在 线程池的原理 中,ThreadPoolExecutor 内部维护了一个 HashSet 类型的 workers 集合,用于存放所有的工作线程。每个工作线程都是 Worker 类的实例,而 Worker 类继承了 AQS 并实现了 Runnable 接口。这种设计使得线程池能够方便地管理线程的生命周期和状态。
理解 线程池的工作原理 是避免并发问题的关键。当一个任务被提交到线程池时,线程池会按照以下逻辑进行处理:
如果当前运行的线程数少于 corePoolSize,则创建一个新的核心线程来执行新提交的任务。即使其他核心线程处于空闲状态,也会优先创建新线程,而不是复用空闲线程。
如果当前运行的线程数等于或大于 corePoolSize,则将新任务放入 workQueue(任务队列)中等待。
如果任务队列已满,且当前运行的线程数少于 maximumPoolSize,则创建一个非核心线程来执行该任务。
如果任务队列已满,且当前运行的线程数等于 maximumPoolSize,则线程池处于饱和状态,将触发 handler(拒绝策略)来处理新提交的任务。
流程顺序:核心线程 → 任务队列 → 非核心线程 → 拒绝策略。理解这一流程,就能解释为什么某些参数配置会导致性能问题或OOM。
正确的参数配置是 线程池的原理 落地的关键。不同的业务场景需要不同的配置策略。
CPU密集型任务是指需要进行大量计算、很少阻塞的任务。例如:复杂的数学运算、图像处理等。
// CPU密集型示例
int cpuCount = Runtime.getRuntime().availableProcessors();
ThreadPoolExecutor executor = new ThreadPoolExecutor(
cpuCount + 1,
cpuCount + 1,
0L,
TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<>(100) // 有界队列,防止OOM
);
IO密集型任务是指需要进行大量IO操作(如网络请求、数据库查询、文件读写)的任务。这类任务大部分时间在等待IO完成,CPU利用率较低。
// IO密集型示例
int ioCount = Runtime.getRuntime().availableProcessors() 2;
ThreadPoolExecutor executor = new ThreadPoolExecutor(
ioCount,
ioCount 2,
60L,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(1000)
);
如果系统中既有CPU密集型任务,又有IO密集型任务,可以将它们分开处理,或者根据任务的优先级进行调度。一般建议将不同性质的任务提交到不同的线程池中,以避免相互影响。
当线程池和任务队列都满时,新任务将被拒绝。Java提供了四种标准的拒绝策略,理解它们的区别对于系统稳定性至关重要。
直接抛出 RejectedExecutionException 异常,阻止系统正常运行。适用于对数据一致性要求极高的场景。
由调用线程(提交任务的线程)处理该任务。这会降低提交任务的速率,起到一种“背压”机制,防止系统过载。
直接丢弃任务,不抛出异常。适用于对数据丢失不敏感的场景,如日志收集。
丢弃队列中最老的任务(即最先入队的任务),然后尝试重新提交当前任务。适用于任务具有时效性,旧任务价值较低的场景。
在实际生产中,我们通常需要自定义拒绝策略,例如将丢弃的任务记录到日志或数据库中,以便后续分析和补偿。
public class CustomRejectPolicy implements RejectedExecutionHandler {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
// 记录日志
System.out.println("任务被拒绝: " + r.toString());
// 可以发送到消息队列或数据库
// logService.saveRejectedTask(r);
}
}
尽管 线程池的原理 看似简单,但在实际应用中,开发者经常会遇到各种坑。以下是一些常见的调优建议。
阿里巴巴Java开发手册明确规定:不允许使用 Executors 去创建线程池,而是通过 ThreadPoolExecutor 的方式。原因如下:
newFixedThreadPool 和 newSingleThreadExecutor:允许请求队列长度为 Integer.MAX_VALUE,可能堆积大量请求,导致OOM。newCachedThreadPool 和 newScheduledThreadPool:允许创建的线程数量为 Integer.MAX_VALUE,可能创建大量线程,导致OOM。在生产环境中,必须对线程池进行监控,以便及时发现和处理问题。关键指标包括:
可以使用 Micrometer、Prometheus 等监控工具将线程池指标暴露出来,结合 Grafana 进行可视化展示。
在应用退出时,必须优雅地关闭线程池,确保所有任务执行完毕或安全丢弃。
// 1. 调用 shutdown(),不再接收新任务,但会执行完已提交的任务
executor.shutdown();
// 2. 等待一段时间
if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
// 3. 如果超时,强制关闭
executor.shutdownNow();
}
因为 newFixedThreadPool 使用的是无界队列 LinkedBlockingQueue,其容量为 Integer.MAX_VALUE。当任务提交速度远大于处理速度时,队列会无限堆积,最终导致内存溢出(OOM)。建议手动创建 ThreadPoolExecutor 并指定有界队列。
主要包括:corePoolSize(核心线程数)、maximumPoolSize(最大线程数)、keepAliveTime(空闲线程存活时间)、unit(时间单位)、workQueue(任务队列)、threadFactory(线程工厂)、handler(拒绝策略)。
CPU密集型任务:核心线程数=CPU核数+1,减少上下文切换。IO密集型任务:核心线程数=2CPU核数或CPU核数/(1-阻塞系数),充分利用IO等待时间。混合型任务需根据具体IO占比调整。
是的,这是线程池的核心优势。线程执行完任务后,不会立即销毁,而是回到线程池中等待下一个任务。只有当线程空闲时间超过 keepAliveTime 且线程数超过 corePoolSize 时,非核心线程才会被销毁。
当线程池中的线程数达到 maximumPoolSize 且任务队列也满了时,线程池处于饱和状态。此时新提交的任务将被拒绝,触发拒绝策略。常见的拒绝策略有 AbortPolicy、CallerRunsPolicy、DiscardPolicy 和 DiscardOldestPolicy。