目录
- 一、为什么要用线程池?
- [二、先认清 JDK 给我们的"开箱即用"线程池](#二、先认清 JDK 给我们的"开箱即用"线程池 "#%E4%BA%8C%E5%85%88%E8%AE%A4%E6%B8%85-jdk-%E7%BB%99%E6%88%91%E4%BB%AC%E7%9A%84%E5%BC%80%E7%AE%B1%E5%8D%B3%E7%94%A8%E7%BA%BF%E7%A8%8B%E6%B1%A0")
- 三、初始化:七个参数与构造器校验
- 四、源码主线:从一个任务的一生说起
- 五、线程的回收与线程池关闭
- 六、参数怎么设、怎么"动态"设
- 七、避坑:几种容易出事"顺手"写法
- [八、一图回顾 + 高频面试题速答](#八、一图回顾 + 高频面试题速答 "#%E5%85%AB%E4%B8%80%E5%9B%BE%E5%9B%9E%E9%A1%BE--%E9%AB%98%E9%A2%91%E9%9D%A2%E8%AF%95%E9%A2%98%E9%80%9F%E7%AD%94")
- 九、结语
一、为什么要用线程池?
线程本身是用来异步处理任务 的,而且可以并发处理多个任务。但如果无限制地 new Thread(),问题就来了:
- 创建/销毁成本高:每次创建线程都要向 OS 申请栈空间、注册线程;
- 切换成本高:线程数远超 CPU 核数时,上下文切换吃掉大量 CPU;
- 不可控 :无法统一限流、监控、管理,极端情况直接
OOM: unable to create new native thread。
于是"池化技术"登场------提前养一批线程,反复复用,按需分配。JDK 文档给出了线程池的四大好处:
| 好处 | 说明 |
|---|---|
| 降低资源消耗 | 重复利用已创建的线程,降低创建和销毁造成的损耗 |
| 提高响应速度 | 任务到达时,不需要等待线程创建,直接 execute 即可 |
| 提高可管理性 | 统一分配、调优、监控(getPoolSize()、getActiveCount()、getTaskCount()) |
| 提供更多强大功能 | 可扩展性:beforeExecute / afterExecute 钩子、UncaughtExceptionHandler、定时调度 |
一句话抓住核心思想:用固定的(上限可控的)线程数量,去消费不定的 task ------ 这就是所有源码要基于它理解的根本。
整体是一个生产者-消费者模型:
二、先认清 JDK 给我们的"开箱即用"线程池
Executors 工具类封装了几种常见线程池:
| 工厂方法 | 底层实现 | 特点 | 风险点(补充) |
|---|---|---|---|
newFixedThreadPool(n) |
ThreadPoolExecutor,core = max = n,LinkedBlockingQueue(无界) |
线程数固定,超出任务排队 | 无界队列 → 任务堆积导致 OOM |
newSingleThreadExecutor() |
同上,core = max = 1 | 单线程串行,适合保证顺序执行 | 同上 |
newCachedThreadPool() |
core = 0,max = Integer.MAX_VALUE,SynchronousQueue,keepAlive = 60s |
有任务即创建,空闲 60s 回收 | 线程数无上限 → 创建线程 OOM |
newScheduledThreadPool(n) |
ScheduledThreadPoolExecutor |
延迟 / 周期执行,类似定时器 | 队列无界 |
newWorkStealingPool() |
ForkJoinPool (不是 ThreadPoolExecutor 的子类) |
工作窃取,适合耗时的并行计算型任务,能充分利用 CPU | 对阻塞型任务不友好 |
关于 newWorkStealingPool:它是 ForkJoinPool 的扩展,适合做可拆分、耗时的并行操作(分治 + 工作窃取,空闲线程去别的队列"偷"任务,减少空闲)。
除了 newWorkStealingPool,其余全是 ThreadPoolExecutor 的一层封装,生产环境直接 new ThreadPoolExecutor(...)。
理由是:
- 逼着你去把
corePoolSize / maximumPoolSize / keepAliveTime / workQueue / handler这几个参数搞清楚; - 能做的事更多------可以挂监控(自定义
afterExecute统计耗时)、自定义拒绝策略、自定义线程名/异常处理器; - 规避
Executors那些"无界"实现的 OOM 风险,这也是阿里《Java 开发手册》里明确要求的一条:
【强制】线程池不允许使用
Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
先跑个最简单的例子感受一下(注意:把 new Thread(runnable) 作为参数传给 execute 其实是不推荐的写法,原因见第七节):
java
ExecutorService executorService = Executors.newFixedThreadPool(2);
for (int i = 0; i < 10; i++) {
executorService.execute(new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "执行任务");
}));
}
executorService.shutdown();
输出只会是 pool-1-thread-1 / pool-1-thread-2 两两交替------10 个任务,只有 2 个线程在干活,这就是"固定线程数消费不定量任务"。
再来看一个带耗时的版本,50 个任务、5 个线程、每个睡 1 秒:
java
ExecutorService executor = Executors.newFixedThreadPool(5);
for (int i = 0; i < 50; i++) {
executor.execute(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("测试" + Thread.currentThread().getName());
});
}
executor.shutdown();
大约每 1 秒打印 5 条,10 秒左右跑完------5 个线程轮番复用,而不是创建 50 个线程。
三、初始化:七个参数与构造器校验
官方给出的参数标准:
java
public ThreadPoolExecutor(int corePoolSize, // 核心线程数
int maximumPoolSize, // 最大线程数
long keepAliveTime, // 存活时间:除核心线程外,空闲线程多久后被回收
TimeUnit unit, // 存活时间单位
BlockingQueue<Runnable> workQueue, // 阻塞队列,待执行的 task 都在这里
ThreadFactory threadFactory, // 线程工厂
RejectedExecutionHandler handler) { // 拒绝策略:队列满 + 线程数达 maximum 时触发
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime); // 统一转成纳秒
this.threadFactory = threadFactory;
this.handler = handler;
}
任务队列怎么用 什么时候用
| 队列 | 特点 | 适用 |
|---|---|---|
ArrayBlockingQueue |
有界,数组实现,一把锁 | 生产首选,可控,能触发扩容到 max 与拒绝 |
LinkedBlockingQueue |
链表实现,读写两把锁,无参构造时容量 = Integer.MAX_VALUE |
想用它也可以,但必须指定容量 |
SynchronousQueue |
不存储元素,put 必须有线程 take | CachedThreadPool 那种"来了就建线程" |
PriorityBlockingQueue |
无界,按优先级出队 | 需要优先级调度(注意 still 无界) |
DelayQueue |
延迟队列,ScheduledThreadPoolExecutor 用它 |
定时任务 |
LinkedTransferQueue |
无锁实现,transfer 语义 | 高吞吐场景 |
划重点:线程池里"最大线程数"能不能用得着,取决于队列是否有界 。用无界
LinkedBlockingQueue时maximumPoolSize形同虚设------因为队列永远offer成功,永远走不到"尝试创建非核心线程"那一步。
四、源码主线:从一个任务的一生说起
4.1 全景流程图
scss
主线程 execute(task)
│
▼
① 核心线程数没满? ── 是 ──► addWorker(task, true) 创建一个核心 Worker 直接执行
│ 否
▼
② 任务成功入队 workQueue.offer()? ── 是 ──► 二次检查 recheck
│ 否 ├─ 池已关闭 → 出队 + 拒绝
▼ └─ 线程数为 0 → addWorker(null, false) 兜底
③ addWorker(task, false) 创建非核心线程(没超过 maximumPoolSize)
│ 失败
▼
④ reject(task) 走拒绝策略
大概就是上述的流程吧
4.2 ctl:一个 int 同时存状态和线程数
先看 JDK 里最"巧"的设计:
java
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
private static final int COUNT_BITS = Integer.SIZE - 3; // 29
private static final int COUNT_MASK = (1 << COUNT_BITS) - 1; // 2^29 - 1
private static final int RUNNING = -1 << COUNT_BITS; // 高 3 位 111
private static final int SHUTDOWN = 0 << COUNT_BITS; // 000
private static final int STOP = 1 << COUNT_BITS; // 001
private static final int TIDYING = 2 << COUNT_BITS; // 010
private static final int TERMINATED = 3 << COUNT_BITS; // 011
private static int runStateOf(int c) { return c & ~COUNT_MASK; } // 取高 3 位
private static int workerCountOf(int c) { return c & COUNT_MASK; } // 取低 29 位
private static int ctlOf(int rs, int wc) { return rs | wc; } // 合并
ctl 的二进制布局(一共 32 位):
text
31 29 28 0
┌────────────┬──────────────────────────────────────────────────┐
│ 高 3 位 │ 低 29 位 │
│ runState │ workerCount │
│ 线程池状态 │ 线程数量 │
└────────────┴──────────────────────────────────────────────────┘
111 RUNNING 000 SHUTDOWN 001 STOP 010 TIDYING 011 TERMINATED
- 高 3 位 = 线程池状态(可表示 8 种,JDK 用了 5 种)
- 低 29 位 = 线程数量 (最大
2^29 - 1 ≈ 5.36 亿)
五个状态的含义与流转:
| 状态 | 值 | 说明 |
|---|---|---|
RUNNING |
111 |
接受新任务,并处理队列中的任务 |
SHUTDOWN |
000 |
不接受新任务,但会把队列中的任务处理完 |
STOP |
001 |
不接受新任务,不处理队列任务,并中断正在执行的任务 |
TIDYING |
010 |
所有任务已终止、workerCount 为 0,即将执行 terminated() |
TERMINATED |
011 |
terminated() 执行完毕 |
scss
RUNNING ──shutdown()──► SHUTDOWN ──队列空且线程数0──► TIDYING ──► TERMINATED
│ │ ▲
└────shutdownNow()──────►└───STOP───线程数0────────────┘
为什么要这么打包? 一次 CAS 就能同时维护"状态 + 线程数",减少锁和 CAS 次数。ThreadPoolExecutor 里只有 workers 集合的操作才用 mainLock(ReentrantLock),ctl 的读写都是无锁的 CAS 自旋------这是高性能并发设计的经典手法。
4.3 execute():三段式提交逻辑
java
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
int c = ctl.get();
// ① 线程数 < corePoolSize,直接创建核心线程执行
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
// ② 线程池处于 RUNNING 且任务入队成功
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
// 入队后二次检查:期间线程池被关闭 → 撤销入队 + 拒绝
if (!isRunning(recheck) && remove(command))
reject(command);
// 二次检查:线程数为 0(比如核心线程刚好全部超时退出)→ 建一个非核心线程兜底
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
// ③ 入队失败(队列满)→ 尝试建非核心线程,还是失败 → 拒绝
else if (!addWorker(command, false))
reject(command);
}
JDK 源码注释里也把它总结为 3 steps。在这里补充一下:
- 为什么入队后要
recheck?- 入队与
shutdown()之间存在竞态:可能刚入队,池子就被关了 → 需要remove把任务撤回并拒绝; - 也可能在检查线程数之后,最后一个工作线程恰好因
getTask()返回 null 而退出 → 线程数为 0,队列里却还有任务,必须补建一个线程 (addWorker(null, false),注意firstTask是 null,只负责消费队列)。
- 入队与
- 为什么
addWorker(null, false)里core = false? 这里只是补个"消费者",不关心是核心还是非核心,传 false 会按maximumPoolSize校验------已经shutdown的情况下(shutdown后队列非空),也会走这条逻辑把剩余任务消费完。 - 顺序为什么是"先核心、再队列、再非核心"? 这是线程池最关键的设计取舍:优先复用 + 缓冲,而不是无脑扩线程。任务堆积优先排队(避免频繁建线程),只有在队列也装满时才扩容到 max,最后才拒绝。容量有限才能形成"背压",让上游感知到。
4.4 addWorker():CAS 抢占 + 加锁入集合 + 启动线程
java
private boolean addWorker(Runnable firstTask, boolean core) {
retry: // goto 标签,配合 break/continue 跳出多层循环
for (int c = ctl.get();;) {
// 检查状态:>= SHUTDOWN 且(>= STOP 或 firstTask 非空 或 队列已空)→ 不能再创建
if (runStateAtLeast(c, SHUTDOWN)
&& (runStateAtLeast(c, STOP) || firstTask != null || workQueue.isEmpty()))
return false;
for (;;) {
// 数量检查:核心线程比 corePoolSize,非核心比 maximumPoolSize
if (workerCountOf(c) >= ((core ? corePoolSize : maximumPoolSize) & COUNT_MASK))
return false;
// CAS 把 workerCount +1
if (compareAndIncrementWorkerCount(c))
break retry;
c = ctl.get(); // CAS 失败,重读 ctl
if (runStateAtLeast(c, SHUTDOWN))
continue retry; // 状态变了,回到外层重新做状态检查
// 否则只是因为并发改线程数导致 CAS 失败,重试内层循环
}
}
boolean workerStarted = false, workerAdded = false;
Worker w = null;
try {
w = new Worker(firstTask); // Worker 实现了 Runnable,内部持有 Thread
final Thread t = w.thread;
if (t != null) {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
int c = ctl.get();
// 持锁后再次检查状态,防止拿到锁之前池子被关
if (isRunning(c) || (runStateLessThan(c, STOP) && firstTask == null)) {
if (t.getState() != Thread.State.NEW)
throw new IllegalThreadStateException();
workers.add(w);
workerAdded = true;
int s = workers.size();
if (s > largestPoolSize)
largestPoolSize = s; // 记录历史峰值,供监控用
}
} finally {
mainLock.unlock();
}
if (workerAdded) {
t.start(); // 启动,会回调 Worker.run() → runWorker()
workerStarted = true;
}
}
} finally {
if (!workerStarted)
addWorkerFailed(w); // 兜底回滚:移除 worker + workerCount-1 + tryTerminate
}
return workerStarted;
}
讲一下 retry: 标签:Java 的 break retry / continue retry 相当于 goto 到外层循环。在下面给一个 goto demo案例:
java
retry:
for (int i = 0; i < 3; i++) {
for (int j = 3; j < 10; j++) {
if (j == 4) {
break retry; // 跳出外层整个循环
}
if (j == 7) {
continue retry; // 跳过 outer 循环当前轮次,i++ 后继续
}
System.out.println(i + ":" + j);
}
}
上面 demo 的
if (j == 4) break retry;一旦命中,后面的j == 7分支就不再会被执行到------两者的执行效果都是"结束内层、进入下一轮外层"。理解retry标签的关键是:标签加在最外层 for 上,break retry= 结束整个双层循环,continue retry= 结束本次外层循环迭代。
这里的设计要点:
- 双层循环各有分工:外层循环只负责"状态检查",内层循环负责"数量检查 + CAS 抢名额"。CAS 失败只重试内层,成本更低;如果状态变了才回到外层。
- CAS 加名额先行,加锁入集合在后 :先在
ctl上原子地"预定"一个线程名额,再去mainLock里workers.add(),这样并发创建不会超过上限。 mainLock保护的是workers这个HashSet以及largestPoolSize/completedTaskCount等统计量,不是保护数量上限(那是 CAS 干的)。
4.5 Worker:一个自带锁的"打工人"
java
private final class Worker extends AbstractQueuedSynchronizer implements Runnable {
final Thread thread;
Runnable firstTask;
volatile long completedTasks;
Worker(Runnable firstTask) {
setState(-1); // inhibit interrupts until runWorker
this.firstTask = firstTask;
this.thread = getThreadFactory().newThread(this);
}
public void run() { runWorker(this); }
// ... tryLock/lock/unlock 都是对 AQS state 的简单包装(0 未锁,1 已锁)
}
setState(-1)是干嘛的呢?
Worker继承AbstractQueuedSynchronizer,用它实现了一把不可重入的互斥锁 ,比ReentrantLock更轻量,而且"不可重入"能防止任务里再去调用池子的控制方法时重复加锁。state = -1是"未启动"的中间态:此时tryLock()永远失败(state != 0),于是interruptIdleWorkers不会中断一个还"没真正跑起来"的线程。runWorker里第一件事就是w.unlock()(把 state 置 0),从此才允许被中断。- 为什么要这把锁?区分"空闲线程"和"正在执行的线程" :只有
tryLock()成功(说明没在跑任务)的 Worker,才允许interrupt()。这样shutdown()派发的中断总是能唤醒阻塞在take()上的空闲线程,而不会打断正在执行的任务。
4.6 runWorker():线程复用的心脏
java
final void runWorker(Worker w) {
Thread wt = Thread.currentThread();
Runnable task = w.firstTask; // 首次执行时可能有 firstTask
w.firstTask = null;
w.unlock(); // allow interrupts:state 从 -1 置 0
boolean completedAbruptly = true;
try {
// ★ 只要还能取到任务就一直循环:这就是"线程复用"的实现
while (task != null || (task = getTask()) != null) {
w.lock(); // 执行期间加锁 → 标记"忙",防止被空闲中断
// 若池子处于 STOP 及以上,确保线程被中断;否则清理掉残留中断标记
if ((runStateAtLeast(ctl.get(), STOP) ||
(Thread.interrupted() && runStateAtLeast(ctl.get(), STOP))) &&
!wt.isInterrupted())
wt.interrupt();
try {
beforeExecute(wt, task); // 钩子:任务前
Throwable thrown = null;
try {
task.run(); // 真正执行任务
} catch (RuntimeException x) { thrown = x; throw x;
} catch (Error x) { thrown = x; throw x;
} catch (Throwable x) { thrown = x; throw new Error(x);
} finally {
afterExecute(task, thrown); // 钩子:任务后(无论是否异常)
}
} finally {
task = null; // 置空,防止内存泄漏
w.completedTasks++; // 累计该 worker 完成数
w.unlock();
}
}
completedAbruptly = false; // 正常退出(getTask 返回 null)
} finally {
processWorkerExit(w, completedAbruptly); // 收尾 + 可能补线程
}
}
关键点:
- 线程不会在任务结束后销毁 ,而是
while循环回到getTask()继续取任务;getTask()返回 null 才是线程结束的唯一正常出口。 completedAbruptly记录"是否因为用户任务抛异常而突然退出":如果是,processWorkerExit会补建一个线程 保证线程数不缩水;如果是正常退出,则按需补充到corePoolSize(或allowCoreThreadTimeOut时补到 0)。catch (Throwable x) { throw new Error(x); }的意义:Runnable.run()的签名不允许抛受检异常,JDK 把任意Throwable包装成Error抛出,同时把原始异常交给afterExecute------所以你看到的"任务异常没有打印",往往是因为你提交的是new Thread(任务)而不是直接提交任务(见第六节避坑)。beforeExecute/afterExecute/terminated是三个可覆写钩子,监控告警就靠它们。
4.7 getTask():阻塞、超时与回收
java
private Runnable getTask() {
boolean timedOut = false; // 上一次 poll 是否超时
for (;;) {
int c = ctl.get();
// 池子已 SHUTDOWN 且(已 STOP 或队列空了)→ 线程数减一,返回 null,线程结束
if (runStateAtLeast(c, SHUTDOWN)
&& (runStateAtLeast(c, STOP) || workQueue.isEmpty())) {
decrementWorkerCount();
return null;
}
int wc = workerCountOf(c);
// 是否允许超时淘汰:allowCoreThreadTimeOut 为 true,或线程数已超过 corePoolSize
boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
// 超时了 / 超最大线程数,且(线程数 > 1 或 队列空)→ 数量减一,返回 null → 该线程结束
if ((wc > maximumPoolSize || (timed && timedOut))
&& (wc > 1 || workQueue.isEmpty())) {
if (compareAndDecrementWorkerCount(c))
return null;
continue; // CAS 失败重试
}
try {
// timed = true:最多等 keepAliveTime,超时返回 null(下面置 timedOut=true)
// timed = false:无超时地阻塞等待
Runnable r = timed
? workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS)
: workQueue.take();
if (r != null)
return r;
timedOut = true; // ★ 这次超时了,下轮循环走到上面就返回 null 退出
} catch (InterruptedException retry) {
timedOut = false; // 被中断不算超时,重新判断状态
}
}
}
这段是整个线程池"能收能放"的核心:
take()是永久阻塞 :核心线程即使闲着也挂在这儿等,不会被回收(除非allowCoreThreadTimeOut(true))。poll(keepAliveTime)是限时阻塞 :超过keepAliveTime还取不到任务就返回 null →timedOut = true→ 下一轮循环命中回收条件 →workerCount-1→ 线程结束、被回收。wc > 1 || workQueue.isEmpty()这个条件很精妙:永远至少保留一个线程 ,避免"队列里刚来任务,最后那个线程却恰好超时退出"导致任务没人消费(即使退出,tryTerminate也会在必要时补一个空闲线程,这里主要是为了避免高频抖动)。
4.8 拒绝策略
线程池内置 4 种 RejectedExecutionHandler:
| 策略 | 行为 | 风险/适用 |
|---|---|---|
AbortPolicy(默认) |
抛 RejectedExecutionException |
让调用方感知并处理,适合核心业务 |
CallerRunsPolicy |
由提交任务的线程自己执行 | 优雅的背压:提交线程被占用,天然降低提交速率,不丢任务 |
DiscardPolicy |
静默丢弃 | 可能假死,务必打日志(需要自己包一层) |
DiscardOldestPolicy |
丢弃队列头(最老的任务)再重试提交 | 只适合"新的更重要"的场景 |
自定义一个拒绝策略:
java
public class ExecJavaTemplate implements RejectedExecutionHandler {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
System.out.println("把任务继续执行完" + r.toString());
executor.execute(r); // ⚠️ 有栈溢出风险,见下方说明
}
}
配合:
java
ThreadPoolExecutor executorService = new ThreadPoolExecutor(2, 3, 10, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<>(3), new ExecJavaTemplate());
for (int i = 0; i < 10; i++) {
executorService.execute(new Thread(() -> {
try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); }
System.out.println(Thread.currentThread().getName() + "执行任务");
}));
}
⚠️ 提醒一下(这段写法有坑,别照抄) :拒绝策略里直接写
executor.execute(r)是危险的 。如果此时池子仍是满的,execute会再次触发拒绝 → 再调用rejectedExecution→ 无限递归 →StackOverflowError;如果并发任务耗时较长,还可能造成长期自旋式的"忙等待"。而且队列再次满时这个任务依然会被拒绝。正确的 "重试"必须满足至少一个条件:限次重试 + 退避(
Thread.sleep/LockSupport.parkNanos) ,或者交给独立的补偿线程池 / MQ 落库。例如:
java
/**
* 限次重试 + 指数退避的拒绝策略。
* 注意:不能直接 executor.execute(r),否则池子仍满时会递归回本方法 → StackOverflowError
*/
public class RetryRejectionHandler implements RejectedExecutionHandler {
private static final Logger log = LoggerFactory.getLogger(RetryRejectionHandler.class);
private final int maxRetry;
public RetryRejectionHandler(int maxRetry) { this.maxRetry = maxRetry; }
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
for (int i = 0; i < maxRetry; i++) {
if (e.isShutdown()) { break; }
try {
if (e.getQueue().offer(r, 50L << i, TimeUnit.MILLISECONDS)) { // 50ms, 100ms, 200ms ...
return;
}
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
break;
}
}
// 兜底:落盘 / 丢到 MQ / 告警,而不是死等
log.warn("[rejected] task dropped after {} retries: {}", maxRetry, r);
}
}
生产上更推荐的做法,按优先级排序:
CallerRunsPolicy做背压:最简单的"不丢任务"方案,代价是提交线程被阻塞,可能会拖慢上游;- 溢出任务落库 / 投递 MQ 异步补偿,保证数据不丢;
- 无论选哪种,都必须打点告警(拒绝次数、队列长度),否则就是静默故障。
五、线程的回收与线程池关闭
5.1 线程回收
触发路径其实只有一条:getTask() 返回 null → runWorker 退出循环 → processWorkerExit 收尾。
具体条件(在 getTask 中判断):
- 池子已
SHUTDOWN且队列已空 → 回收; - 池子已
STOP(及以上,shutdownNow之后)→ 回收; timed && timedOut(keepAliveTime到期且队列空)→ 回收;wc > maximumPoolSize(比如setMaximumPoolSize调小了)→ 回收。
所以"核心线程永不回收"其实是可以打破的:
executor.allowCoreThreadTimeOut(true),此时timed恒为 true,核心线程闲置超过keepAliveTime也会被回收(注意:keepAliveTime必须 > 0,否则抛IllegalArgumentException)。
5.2 shutdown()
java
public void shutdown() {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
checkShutdownAccess();
advanceRunState(SHUTDOWN); // 状态迁移到 SHUTDOWN:不再接受新任务
interruptIdleWorkers(); // 只中断"空闲"的 worker(被锁住 = 忙,不动)
onShutdown(); // 钩子,ScheduledThreadPoolExecutor 在这里取消延迟任务
} finally {
mainLock.unlock();
}
tryTerminate();
}
课件强调的一点非常关键:shutdown() 不会终止正在运行的线程。
它只做三件事:
- 把状态改成
SHUTDOWN,之后execute提交的新任务会被拒绝; interruptIdleWorkers()只中断空闲 的 worker------靠的是w.tryLock()成功(见 4.5),正在执行任务的线程不会被中断,它会把手上的任务执行完;tryTerminate()尝试推进到终止状态。
之后那些空闲线程在 getTask() 里被唤醒,发现 SHUTDOWN && 队列空,返回 null 而自然结束。队列中已有的任务会被执行完 ------所以 shutdown() 是"优雅关闭"。
5.3 shutdownNow()
java
public List<Runnable> shutdownNow() {
List<Runnable> tasks;
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
checkShutdownAccess();
advanceRunState(STOP); // 直接到 STOP
interruptWorkers(); // 中断所有 worker(包括正在执行的)
tasks = drainQueue(); // 把队列里没执行的任务全部取出返回
} finally {
mainLock.unlock();
}
tryTerminate();
return tasks;
}
shutdown和shutdownNow之间的对比:
shutdown() |
shutdownNow() |
|
|---|---|---|
| 状态 | SHUTDOWN |
STOP |
| 接受新任务 | 否 | 否 |
| 队列中任务 | 继续执行完 | 不再执行 (drainQueue 返回给你) |
| 正在执行的任务 | 不中断 | 尝试中断 (靠 Thread.interrupt,不响应中断的任务依然跑着) |
| 返回 | void | List<Runnable> 未执行任务 |
补充 :
shutdown()/shutdownNow()都是异步 的,返回时线程池并未真正终止。要等待终止请用awaitTermination(timeout, unit)
5.4 状态流转收口:tryTerminate()
java
final void tryTerminate() {
for (;;) {
int c = ctl.get();
// RUNNING、已 TIDYING 及以上、或 SHUTDOWN 但队列非空 → 不能终止
if (isRunning(c) || runStateAtLeast(c, TIDYING)
|| (runStateLessThan(c, STOP) && !workQueue.isEmpty()))
return;
if (workerCountOf(c) != 0) { // 还有线程 → 只中断一个空闲 worker 传播信号
interruptIdleWorkers(ONLY_ONE);
return;
}
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
if (ctl.compareAndSet(c, ctlOf(TIDYING, 0))) { // TIDYING
try {
terminated(); // 钩子
} finally {
ctl.set(ctlOf(TERMINATED, 0)); // TERMINATED
termination.signalAll(); // 唤醒所有 awaitTermination 的线程
}
return;
}
} finally {
mainLock.unlock();
}
}
}
interruptIdleWorkers(ONLY_ONE) 的注释解释得很清楚:只中断一个空闲线程 ,靠这个被唤醒的线程继续检查状态并退出,进而再次触发 tryTerminate,形成"接力式"的终止传播。shutdown() 之所以中断全部空闲线程,是为了让多余的线程尽快退出,不用等某个"拖后腿"的任务。
顺带一提 shutdown() 里的 interruptIdleWorkers(),真实实现就是一个 tryLock 判断:
java
private void interruptIdleWorkers(boolean onlyOne) {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
for (Worker w : workers) {
Thread t = w.thread;
if (!t.isInterrupted() && w.tryLock()) { // tryLock 成功 = 此刻空闲
try {
t.interrupt(); // 唤醒阻塞在 take() 上的线程
} catch (SecurityException ignore) {
} finally {
w.unlock();
}
}
if (onlyOne) break;
}
} finally {
mainLock.unlock();
}
}
这十几行代码就是"shutdown 不会杀正在跑的任务"的答案 :runWorker 在执行任务期间持有 w.lock(),tryLock() 必然失败,所以忙碌的 worker 会被跳过。
六、参数怎么设、怎么"动态"设
6.1 线程数到底设多少?
按照经典的阻塞系数版本:
scss
线程数 = CPU 可用核心数 / (1 - 阻塞系数)
阻塞系数 = 阻塞时间 / (阻塞时间 + 计算时间) // 取值 (0, 1)
结论:
- CPU 密集型 (
W/C ≈ 0):线程数 ≈N_cpu + 1(或直接N_cpu),线程多了只会增加上下文切换; - IO 密集型 (
W/C大):线程数可以远大于核数,N_cpu × (1 + W/C); - 线程数最少也要 >= CPU 核心数(除非全部是长阻塞等待型);
- 公式只是起点,压测才是终点 :线上还要看 GC、上下文切换(
vmstat/pidstat -w)、CPU 使用率、队列堆积情况反复调优。
6.2 三组典型参数(补充,可直接抄)
java
// ① CPU 密集型:核心数 + 1,有界队列要小,避免任务积压
int cpu = Runtime.getRuntime().availableProcessors();
new ThreadPoolExecutor(cpu + 1, cpu + 1, 0L, TimeUnit.MILLISECONDS,
new ArrayBlockingQueue<>(1024), new NamedThreadFactory("cpu-pool"),
new ThreadPoolExecutor.AbortPolicy());
// ② IO 密集型:按 1 + W/C 放大(这里取 2 倍核数起步,靠压测微调)
new ThreadPoolExecutor(cpu * 2, cpu * 4, 60L, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(2048), new NamedThreadFactory("io-pool"),
new ThreadPoolExecutor.CallerRunsPolicy());
// ③ 定时/延迟任务:用 ScheduledThreadPoolExecutor,别手写 Thread.sleep 轮询
new ScheduledThreadPoolExecutor(2, new NamedThreadFactory("sched-pool"));
NamedThreadFactory(强烈建议自定义,排查问题能一眼看出是哪个池):
java
public class NamedThreadFactory implements ThreadFactory {
private final AtomicInteger seq = new AtomicInteger();
private final String prefix;
public NamedThreadFactory(String prefix) { this.prefix = prefix; }
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r, prefix + "-" + seq.incrementAndGet());
t.setDaemon(false);
t.setUncaughtExceptionHandler((th, e) -> {
System.err.println("[thread-pool] uncaught in " + th.getName());
e.printStackTrace();
});
return t;
}
}
6.3 动态调参
由于 corePoolSize / maximumPoolSize / keepAliveTime / handler 都能在运行时修改(volatile + setter),结合配置中心(Nacos/Apollo)做热更新是非常常见的实践:
java
@Component
public class DynamicThreadPoolManager {
private final ThreadPoolExecutor executor;
public DynamicThreadPoolManager() {
this.executor = new ThreadPoolExecutor(8, 16, 60, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(1024), new NamedThreadFactory("dynamic-pool"),
new ThreadPoolExecutor.CallerRunsPolicy());
refresh(8, 16);
}
/** 配置中心变更回调 */
public void refresh(int core, int max) {
// ① 先定 max(前提 core <= max),② 再设 core ------ 顺序写反会抛 IllegalArgumentException
if (max >= core) {
executor.setMaximumPoolSize(max);
executor.setCorePoolSize(core);
}
}
}
动态调参的关键约束(很多人踩坑),对着 JDK 源码看更清楚:
java
public void setCorePoolSize(int corePoolSize) {
if (corePoolSize < 0 || maximumPoolSize < corePoolSize) // ← 校验相对当前的 max
throw new IllegalArgumentException();
int delta = corePoolSize - this.corePoolSize;
this.corePoolSize = corePoolSize;
if (workerCountOf(ctl.get()) > corePoolSize)
interruptIdleWorkers(); // 线程多了:唤醒空闲线程,让它们在 getTask 里退出
else if (delta > 0) {
// 线程要多少"没人知道",按队列现有任务数预热,队列空了就停
int k = Math.min(delta, workQueue.size());
while (k-- > 0 && addWorker(null, true)) {
if (workQueue.isEmpty()) break;
}
}
}
public void setMaximumPoolSize(int maximumPoolSize) {
if (maximumPoolSize <= 0 || maximumPoolSize < corePoolSize) // ← 必须 >= 当前 core
throw new IllegalArgumentException();
this.maximumPoolSize = maximumPoolSize;
if (workerCountOf(ctl.get()) > maximumPoolSize)
interruptIdleWorkers(); // 线程数超了新上限:多出来的会被回收
}
- 顺序 :必须先
setMaximumPoolSize再setCorePoolSize。因为setCorePoolSize校验的是"当前的maximumPoolSize",如果先调大 core 而 max 还是旧的小值,直接抛IllegalArgumentException。反过来"整体缩容"(core、max 都变小)时,先设 core(此时 max 还是大的,合法),再设 max。一句话记:先把上限撑开,再设下限。 - 改小 core 不会立刻杀线程 :
setCorePoolSize只是interruptIdleWorkers()唤醒空闲线程,再由getTask里的timed判断(wc > corePoolSize)让它们自然退出,正在跑任务的线程会把手上的活干完。 - 调大 core 时会按队列长度预热 :
Math.min(delta, workQueue.size())个新线程,队列空了就提前停止,避免白白创建空闲线程。 allowCoreThreadTimeOut(true)可以让核心线程也"弹性" :适合流量波峰波谷明显的场景,注意keepAliveTime必须 > 0。
监控:用钩子 + 统一打点
java
new ThreadPoolExecutor(core, max, keepAlive, unit, queue, factory, handler) {
@Override
protected void afterExecute(Runnable r, Throwable t) {
if (t != null) { metrics.error("task failed", t); } // 任务异常计数
}
@Override
protected void beforeExecute(Thread t, Runnable r) { metrics.startTimer(); }
};
另外包一层 RejectedExecutionHandler 统计拒绝次数,定时采集 getPoolSize() / getActiveCount() / getQueue().size() / getLargestPoolSize() / getCompletedTaskCount()。
线上必须盯的指标:poolSize、activeCount、queue.size()、largestPoolSize、completedTaskCount、拒绝次数。队列持续增长 / 拒绝率上升,就意味着该扩容或该检查是不是有慢任务。
更好的选择 :如果允许引依赖,直接上美团的 DynamicTp 或 hippo4j 这类动态线程池框架,内置了监控、告警、配置热更新,省得自己造轮子。
七、避坑:几种容易出事的"顺手"写法
这部分是最容易被面试官问、也最容易在线上出事的地方。
7.1 不要直接 execute(new Thread(runnable))
网上很多示例会写成 executorService.execute(new Thread(() -> {...}))。这样写:
- 线程池只是拿着一个
Thread对象在"执行"它 ,真正的业务跑在new Thread自己的线程里; - 线程池的参数(线程数、队列)基本失效,等于每个任务都新建线程,池化完全白费;
- 异常也在那个新线程里抛出,线程池的
UncaughtExceptionHandler抓不到。
正确写法永远是 executor.execute(() -> {...}) 或 executor.submit(...)。
7.2 submit 会吃掉异常
submit() 返回 Future,任务里抛的异常被包进 Future,只有调用 future.get() 才会抛出来。用 submit 却不 get,异常就"人间蒸发"了。要么用 execute,要么每次 submit 都记得处理 Future。
7.3 线程池里的 ThreadLocal 不会自动清理
线程复用意味着 ThreadLocal 会跟着线程一直存在,任务结束前务必 remove(),否则脏数据会串到下一个任务,甚至OOM。
7.4 记得关闭线程池
shutdown()(优雅)/ shutdownNow()(立即)+ awaitTermination()。Spring 环境里可以注册 @PreDestroy 或 DisposableBean,或者用 Spring 的 ThreadPoolTaskExecutor(destroy() 会内部调 shutdown)。
7.5 任务要响应中断
shutdownNow() 只是 interrupt(),任务里如果是 while (true) { ... } 不检查中断,就永远停不下来。阻塞操作优先用可中断 API(put/take/poll/Lock.lockInterruptibly)。
八、一图回顾 + 高频面试题速答
一次提交任务的完整路径
scss
execute(cmd)
├─ workerCount < corePoolSize ──► addWorker(cmd, true) ──► new Worker + t.start() ──► runWorker
├─ isRunning && queue.offer(cmd) ──► recheck:关闭了→remove+reject;线程数0→addWorker(null,false)
└─ else addWorker(cmd, false) 失败 ──► reject(cmd) ──► Abort / CallerRuns / Discard / DiscardOldest / 自定义
runWorker: while ((task = getTask()) != null) { lock(); task.run(); unlock(); }
getTask: (take | poll(keepAliveTime)) → 空且超时 → return null → 线程结束回收
shutdown: SHUTDOWN + 中断空闲 worker + 消费完队列 → TIDYING → TERMINATED
| 问题 | 要点 |
|---|---|
| 线程池为什么能复用线程? | runWorker 里的 while 循环 + getTask() 阻塞取任务,任务结束不退出线程 |
| 核心线程会回收吗? | 默认不回收;allowCoreThreadTimeOut(true) 后按 keepAliveTime 回收 |
为什么 maximumPoolSize 经常不生效? |
队列无界(如默认 LinkedBlockingQueue)时永远 offer 成功,走不到扩容 |
为什么要 recheck? |
入队与 shutdown / 线程退出存在竞态,需要撤销入队或补建线程 |
| 线程抛异常了池子会怎样? | completedAbruptly=true → processWorkerExit 补建线程;但 submit 会吞异常 |
shutdown 和 shutdownNow 区别? |
SHUTDOWN 执行完队列 vs STOP 中断 + 丢弃队列并返回 |
为什么用 ctl 一个变量? |
一次 CAS 同时更新状态+线程数,无锁且高效 |
九、结语
线程池的本质其实就一句话:用上限可控的线程集合,配合一个阻塞队列,把"不定量、突发式"的任务变成"稳定、可观测"的执行流。 源码里所有的技巧------ctl 位运算、双层自旋 CAS、Worker 内置不可重入锁、recheck 二次检查------都是围绕"并发下的正确性 + 低开销"做的取舍。
最后三条实践建议:
- 生产禁用
Executors,手写ThreadPoolExecutor,队列必须有界,线程必须命名; - 核心池、最大池、队列容量三者的容量关系要画出来算清楚 ,再做压测。在任务持续提交(未执行完)的瞬间,最大可接收任务数 =
corePoolSize + queue.capacity + (maximumPoolSize - corePoolSize),超过即拒绝。以core=2, max=4, ArrayBlockingQueue(2)为例,最多只能吃下2 + 2 + 2 = 6个任务,第 7 个就开始抛RejectedExecutionException。(注:若有任务在这期间执行完毕,后续任务可以继续提交,所以这给的是"瞬时容量"。) - 上监控:队列长度、活跃线程数、拒绝率是线程池的三大生命体征,必要上动态调参。
有什么问题欢迎大家评论区留言!!!