Java线程池详解
参数及工作原理
线程池是并发编程中非常重要的工具,它能有效管理线程资源,提升系统性能。我们以Java的ThreadPoolExecutor为例来说明,这是最经典、最通用的实现。
一、七大核心参数
以 ThreadPoolExecutor 的构造函数为例:
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的时间单位(秒、毫秒等)。 |
| workQueue | 任务阻塞队列。用于存放等待执行的任务(Runnable)。 |
| threadFactory | 线程工厂。用于创建新线程,可自定义线程名、优先级等。 |
| handler | 拒绝策略。当队列满且线程数已达maximumPoolSize时,对新提交的任务采取的处理策略。 |
二、工作原理(任务提交的执行流程)
当一个任务通过 execute() 或 submit() 提交到线程池后,执行流程如下(核心逻辑):
更详细的文字步骤:
-
判断核心线程数
- 当前运行的线程数 <
corePoolSize:直接创建新线程(核心线程)来执行该任务。 - 当前运行的线程数 >=
corePoolSize:进入下一步。
- 当前运行的线程数 <
-
判断任务队列是否已满
- 任务队列
workQueue未满:将任务放入队列中,等待核心线程空闲后拉取执行。 - 任务队列 已满:进入下一步。
- 任务队列
-
判断总线程数是否达最大线程数
- 当前运行的线程数 <
maximumPoolSize:创建新线程(非核心线程,可超时销毁)立即执行任务。 - 当前运行的线程数 >=
maximumPoolSize:线程池已饱和,执行拒绝策略。
- 当前运行的线程数 <
-
非核心线程的超时销毁
- 当非核心线程空闲时间超过
keepAliveTime后,会被终止回收(直到线程数回落到corePoolSize)。
- 当非核心线程空闲时间超过
三、常见的工作队列(workQueue)特点
| 队列类型 | 特点 | 适用场景 |
|---|---|---|
SynchronousQueue |
不存储任务,每个插入必须等待一个移除。容量为0,常用于将任务直接交给线程执行。 | 要求高吞吐量、快速响应的场景,配合maximumPoolSize较大时。 |
ArrayBlockingQueue |
有界数组队列,容量固定。 | 资源控制严格,防止队列无限膨胀。 |
LinkedBlockingQueue |
链表结构,默认无界(Integer.MAX_VALUE),也可设为有界。 |
任务积压容忍度高,但无界时需注意内存风险。 |
PriorityBlockingQueue |
支持优先级排序的无界队列。 | 需要按优先级执行的任务。 |
四、常见拒绝策略(handler)
Java 提供了四种内置策略:
| 策略 | 行为 | 风险 / 场景 |
|---|---|---|
AbortPolicy(默认) |
直接抛出 RejectedExecutionException |
明确拒绝,需要上游捕获处理。 |
CallerRunsPolicy |
由提交任务的线程(调用者)自己执行该任务 | 放缓任务提交速度,提供反压机制。 |
DiscardPolicy |
静默丢弃该任务,不抛异常 | 允许丢失不重要任务。 |
DiscardOldestPolicy |
丢弃队列中最老的任务,然后重新提交当前任务 | 追求最新任务的处理。 |
五、如何合理设置参数?
- CPU 密集型任务:
corePoolSize= CPU 核心数 + 1(避免过多线程竞争CPU)。 - I/O 密集型任务:
corePoolSize= CPU 核心数 × 2(或更高,因为大部分时间在等待I/O)。 - 队列大小:根据任务到达率、任务处理时长、允许的最大等待时间计算。一般建议有界队列,防止内存溢出。
- 拒绝策略:根据业务对丢失任务的容忍度选择,默认
AbortPolicy较安全。
六、一个简单的例子
// 创建一个线程池:核心2线程,最大4线程,队列容量3,空闲线程存活1秒
ThreadPoolExecutor pool = new ThreadPoolExecutor(
2, 3, 1L, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(3),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy()
);
for (int i = 0; i < 8; i++) {
final int taskId = i;
pool.execute(() -> {
System.out.println(Thread.currentThread().getName() + " 执行任务 " + taskId);
try { Thread.sleep(100); } catch (InterruptedException e) {}
});
}
pool.shutdown();
执行时会看到:前2个任务立即创建核心线程执行,第3-5个任务进入队列,第6-7个任务创建非核心线程执行,第8个任务触发拒绝策略(抛出异常)。
七、不推荐使用 Executors 工厂类的原因
Executors.newFixedThreadPool和newSingleThreadExecutor使用无界队列,高负载下可能导致 OOM。Executors.newCachedThreadPool最大线程数为Integer.MAX_VALUE,可能创建过多线程导致系统资源耗尽。
所以生产环境建议直接使用 ThreadPoolExecutor 并明确指定参数。
如果你正在设计具体业务的线程池(比如异步处理、定时任务或结合虚拟线程),可以告诉我场景,我能给更针对性的参数建议。
怎么实现线程的重复使用
核心思路
线程池实现线程重复使用的核心原理是:让每个工作线程内部维持一个循环,不断从任务队列中取出新任务来执行,而不是执行完一个任务后就退出。
下面以 Java 的 ThreadPoolExecutor 为例,详细拆解其实现机制。
一、关键角色:Worker 和 runWorker
线程池内部把每个工作线程封装成一个 Worker 对象。Worker 本身是一个 Runnable,它持有一个 Thread 对象。
private final class Worker extends AbstractQueuedSynchronizer implements Runnable {
final Thread thread; // 真正执行的线程
Runnable firstTask; // 创建时可能附带的一个初始任务
// ...
}
当你提交任务时,线程池会根据规则创建 Worker(也就是创建新线程)。创建后,该 Worker 的 thread 会启动,并运行 Worker 的 run 方法,最终进入 runWorker 方法。
二、核心循环:runWorker 方法(简化版)
final void runWorker(Worker w) {
Thread wt = Thread.currentThread();
Runnable task = w.firstTask; // 先取第一个任务
w.firstTask = null;
while (task != null || (task = getTask()) != null) {
// 在这里执行任务
task.run(); // 直接调用任务的 run 方法,不创建新线程
task = null; // 执行完后清空,然后循环 getTask()
}
// 跳出循环后,线程销毁(回收)
}
关键点:
while循环会一直运行,直到getTask()返回null。- 每执行完一个任务,线程不会退出,而是继续调用
getTask()从任务队列中获取下一个任务。 - 获取到新任务后,再次调用
task.run()—— 同一个线程执行了不同任务,这就是“重复使用”。
三、任务获取:getTask() 如何工作?
getTask() 从 workQueue(阻塞队列)中取出任务。它的伪逻辑是:
private Runnable getTask() {
boolean timedOut = false; // 上次 poll 是否超时
for (;;) {
// 判断当前线程数是否超过 corePoolSize 且允许超时回收
boolean timed = allowCoreThreadTimeOut ||
wc > corePoolSize;
if (timed && (wc > maximumPoolSize || (timedOut && wc > 1))) {
// 如果允许超时且超时发生,减少线程计数并返回 null
return null;
}
Runnable r;
if (timed) {
// 非核心线程:最多等 keepAliveTime,超时返回 null
r = workQueue.poll(keepAliveTime, unit);
if (r == null) timedOut = true;
} else {
// 核心线程:一直阻塞直到有任务
r = workQueue.take();
}
if (r != null) return r;
// 如果 r == null 且 timed == true,循环继续,下次判断会触发返回 null
}
}
- 核心线程:调用
workQueue.take(),队列为空时就一直阻塞等待,不会销毁。 - 非核心线程(或开启了
allowCoreThreadTimeOut的核心线程):调用workQueue.poll(keepAliveTime, unit),等待一段时间后若仍没有新任务,就返回null,导致runWorker的while循环结束,线程自然销毁。
四、整体复用流程总结
[线程池] 内部持有 Worker 线程(例如 3 个)
┌─────────────┐
│ Worker 1 │ ─── while(true) {
│ │ task = queue.take(); // 阻塞拿任务
│ Thread A │ task.run(); // 执行
└─────────────┘ }
┌─────────────┐
│ Worker 2 │ ─── 同样循环
│ Thread B │
└─────────────┘
↑
│ 提交多个任务(R1,R2,R3...)
│
┌─────┴─────┐
│ 工作队列 │
│BlockingQueue│
└───────────┘
- 循环:每个工作线程都在不停循环:
取任务 → 执行任务 → 再取任务。 - 阻塞:队列为空时,线程要么阻塞(核心线程),要么等待超时后退出(非核心线程)。
- 不创建新线程:执行
task.run()时,并没有new Thread(task).start(),而是直接用当前线程执行,所以不会产生额外的线程开销。
五、一个直观的比喻
把线程池想象成一个餐厅的固定服务员团队:
- 每位服务员(线程)一直在餐厅里转悠。
- 如果没有客人(任务),服务员就站在取菜口(阻塞队列)前等待。
- 客人下单后,服务员从取菜口拿到单子(
task),然后去执行(上菜、结账)。 - 做完这单后,服务员马上回到取菜口等待下一个单子,而不是辞职走人。
- 只有在餐厅决定裁员(线程数超过
corePoolSize且空闲太久)时,某些服务员才会离开(线程销毁)。
这就是“重复使用”的本质:线程的生命周期被延长,一直循环获取并执行新任务。
更多推荐



所有评论(0)