java线程池
Java 中的线程池(Thread Pool)是多线程编程的核心技术之一。它是一种基于池化思想管理线程的工具,旨在减少创建和销毁线程的开销,提高系统资源的利用率和吞吐量。
以下是关于 Java 线程池的系统性介绍,涵盖其原理、核心参数、工作流程、创建方式及最佳实践。
一、 为什么使用线程池?(优势)
在没有线程池之前,每次需要执行异步任务时都需要 new Thread()。这样做存在以下问题:
- 开销大:频繁创建和销毁线程会消耗大量的系统资源(内存和 CPU)。
- 无序/不可控:线程无限制创建会占满系统内存,导致
OutOfMemoryError(OOM) 甚至系统崩溃。 - 缺乏管理:无法提供定时执行、定期执行、线程中断等功能。
使用线程池的好处:
- 降低资源消耗:重复利用已创建的线程,减少线程创建和销毁的消耗。
- 提高响应速度:任务到达时,不需要等待线程创建就能立即执行。
- 提高线程的可管理性:线程是稀缺资源,使用线程池可以进行统一分配、调优和监控。
二、 线程池的核心子类:ThreadPoolExecutor
Java 中的线程池核心实现类是 java.util.concurrent.ThreadPoolExecutor。理解这个类的 7 个核心参数 是掌握线程池的关键。
1. 核心参数详解(重点)
java
public ThreadPoolExecutor(
int corePoolSize, // 1. 核心线程数
int maximumPoolSize, // 2. 最大线程数
long keepAliveTime, // 3. 空闲线程存活时间
TimeUnit unit, // 4. 时间单位
BlockingQueue<Runnable> workQueue, // 5. 任务队列
ThreadFactory threadFactory, // 6. 线程工厂
RejectedExecutionHandler handler // 7. 拒绝策略
)
corePoolSize(核心线程数):- 线程池中常驻的线程数。即使这些线程处于空闲状态,也不会被销毁(除非设置了
allowCoreThreadTimeOut)。
- 线程池中常驻的线程数。即使这些线程处于空闲状态,也不会被销毁(除非设置了
maximumPoolSize(最大线程数):- 线程池允许创建的最大线程数量。当队列满时,会继续创建线程,直到达到这个值。
keepAliveTime(空闲存活时间):- 当线程数大于
corePoolSize时,多余的空闲线程在终止前等待新任务的最长时间。
- 当线程数大于
unit(时间单位):keepAliveTime的时间单位(如TimeUnit.SECONDS)。
workQueue(任务队列):- 存放待执行任务的阻塞队列。常见的有:
ArrayBlockingQueue:基于数组的有界阻塞队列。LinkedBlockingQueue:基于链表的无界(或有界)阻塞队列。SynchronousQueue:不存储元素的阻塞队列,每个插入操作必须等待一个移出操作。
- 存放待执行任务的阻塞队列。常见的有:
threadFactory(线程工厂):- 用于创建新线程,可以自定义线程名称、守护进程状态等。
handler(拒绝策略):- 当线程池和队列都满了,如何处理新提交的任务。
三、 线程池的工作流程(非常重要)
当一个新任务提交到线程池时,处理流程如下:
- 步骤 1:如果当前运行的线程数小于
corePoolSize,则直接创建核心线程来执行任务。 - 步骤 2:如果当前运行的线程数大于等于
corePoolSize,则将任务放入任务队列workQueue。 - 步骤 3:如果任务队列已满,且当前运行的线程数小于
maximumPoolSize,则创建非核心线程来执行任务。 - 步骤 4:如果队列满了,且线程数已经达到了
maximumPoolSize,则触发拒绝策略。
记忆口诀:先核心,再队列,后最大,最后拒绝。
四、 拒绝策略 (RejectedExecutionHandler)
JDK 提供了 4 种内置的拒绝策略:
AbortPolicy(默认):直接抛出RejectedExecutionException异常,阻止系统正常运行。CallerRunsPolicy:“调用者运行”机制。用调用者所在的线程(通常是主线程)来执行任务。这会降低新任务的提交速度,起到限流作用。DiscardPolicy:直接丢弃任务,不予处理也不抛出异常。DiscardOldestPolicy:丢弃队列中最老的任务(最先进入队列的任务),然后尝试重新提交当前任务。
五、 常见的四种快捷创建线程池(不推荐在生产使用)
Java 的 Executors 类提供了创建几种常用线程池的静态方法:
newCachedThreadPool:- 特点:可缓存线程池,核心线程数为 0,最大线程数为
Integer.MAX_VALUE。 - 缺点:允许创建的线程数量太多,可能导致 CPU 100% 或 OOM。
- 特点:可缓存线程池,核心线程数为 0,最大线程数为
newFixedThreadPool(int nThreads):- 特点:固定大小的线程池。
- 缺点:任务队列是无界的(
LinkedBlockingQueue),如果任务堆积过多会导致 OOM。
newSingleThreadExecutor:- 特点:单线程线程池,保证任务按顺序执行。
- 缺点:同样使用无界队列,存在 OOM 风险。
newScheduledThreadPool:- 特点:支持定时及周期性任务执行。
⚠️ 【避坑指南】阿里巴巴《Java开发手册》强制规定:
线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式显式创建。
避免因为默认配置的队列长度为Integer.MAX_VALUE(无界)或者最大线程数为Integer.MAX_VALUE导致内存溢出。
六、 推荐的线程池创建与使用方式
在实际开发中,应该使用自定义的 ThreadPoolExecutor。
java
import java.util.concurrent.*;
public class MyThreadPool {
public static void main(String[] args) {
// 获取 CPU 核心数
int cpuCores = Runtime.getRuntime().availableProcessors();
// 1. 手动创建线程池
ThreadPoolExecutor executor = new ThreadPoolExecutor(
cpuCores, // 核心线程数
cpuCores * 2, // 最大线程数
60L, TimeUnit.SECONDS, // 空闲线程存活时间
new LinkedBlockingQueue<>(100), // 有界队列,容量 100
Executors.defaultThreadFactory(), // 线程工厂
new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略
);
// 2. 提交任务
for (int i = 0; i < 10; i++) {
final int index = i;
executor.execute(() -> {
System.out.println(Thread.currentThread().getName() + " 正在执行任务 " + index);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
// 3. 关闭线程池
executor.shutdown(); // 优雅关闭,不再接受新任务,等待现有任务执行完
// executor.shutdownNow(); // 强行关闭,尝试中断正在执行的任务,返回未执行的任务列表
}
}
七、 线程池的参数如何配置(如何确定线程数)?
线程池的大小配置主要取决于任务的性质,一般分为两类:
1. CPU 密集型任务(如:加密、大数计算、压缩)
- 特点:主要是 CPU 在进行计算,没有或很少有 I/O 阻塞。
- 合理线程数:
CPU 核心数 + 1。 - 原因:CPU 满载运转,再多的线程只会增加线程上下文切换的开销。
+1是为了防止某个线程偶尔暂停(如缺页中断)而导致的 CPU 空闲。
2. I/O 密集型任务(如:网络请求、数据库读写、文件读写)
- 特点:系统大部分时间在等待 I/O 操作完成,此时 CPU 处于空闲状态。
- 合理线程数:
CPU 核心数 * 2,或者更精确的公式: - 经验值:在实际 Web 应用(如 Spring Boot)中,通常配置为核心数的 2 到 4 倍(例如 16 或 32),具体需要通过压测微调。
总结
- 核心逻辑:线程池优先使用核心线程,其次放进阻塞队列,最后创建非核心线程。
- 生产禁用:不要用
Executors快捷创建,一律用new ThreadPoolExecutor(...)。 - 监控与调优:合理设置有界队列大小和拒绝策略,并在线上对线程池的活跃线程数、队列积压数进行监控。
右滑查看面试常问