ThreadPoolExecutor:接纳、ctl、Worker 与过载恢复
Java SE 17 / OpenJDK 17u
jdk-17.0.12+7。已核对该标签的ThreadPoolExecutor实现;本机实验运行 Oracle JDK 17.0.12。这里的字段与内部类属于固定版本实现,不能据此声明所有 Java 版本一致。U09 补强关闭协议、拒绝分支与任务异常出口,并把实验入口迁移到 PoolAsyncProbe;锁与中断获取语义见锁语义与公平性,Thread.interrupt协议与取消协作见中断协议与上下文。「换更轻的线程不解决容量」的虚拟线程版论证(Semaphore 限流、pinned carrier 账单、无界 Executor 的接纳责任)在虚拟线程正文,本页池语义仍是固定池模型的权威。
线程池控制的是资源和接纳边界
线程池把提交与执行分离,复用工作线程,保存待执行任务并定义过载出口。它不替业务保证持久化、事务提交或恰好执行一次。execute 没有抛异常,不代表任务已经执行,甚至不必然代表任务被保留:拒绝处理器可能静默丢弃。submit 返回 Future,只提供一个结果观察入口,不能把它当作可靠任务记录。
分析一个池先列六项:线程上限、队列上限、单任务保留对象、任务截止时间、下游连接与业务失败处理。100 个工作线程同时等待 10 个数据库连接,不会产生 100 路有效数据库并行;调大线程数可能只是把连接排队转成更多线程栈与上下文切换。ThreadPoolExecutor API 区分核心数、最大数、排队策略和拒绝,必须组合阅读。
execute 的每一条分支
固定源码是 ThreadPoolExecutor.java。正常主链为 execute → addWorker → Worker.run → runWorker → getTask;退出走 processWorkerExit → tryTerminate / addWorker。以下是按真实控制流精简的教学伪代码,辅助判定改写,不是可替换 JDK 的实现:
读取ctl快照
若workerCount < core:
尝试addWorker(当前任务, 按core限制),成功即返回
失败则重读ctl
若仍RUNNING且queue.offer成功:
再读取ctl
若已关闭且remove(当前任务)成功:拒绝
否则若workerCount为0:addWorker(null, 按max限制)
否则:
尝试addWorker(当前任务, 按max限制),失败则拒绝核心数没满时,任务作为 Worker.firstTask 直接执行,不先放队列。达到核心数后调用非阻塞 offer;只有队列不能接纳才尝试扩到 max。因此“先扩到 max 再排队”是常见错误。使用默认容量极大的 LinkedBlockingQueue 时,队列通常先持续增长,max 无法起到期待中的扩容效果。
入队复查有两个独立作用。提交者第一次看到 RUNNING 后,另一个线程可能执行 shutdown;若任务已经入队但仍能移除,就转入拒绝。remove 失败可能表示 Worker 已经取走,不能再声称任务绝对没执行。另一个作用是入队后若工作线程数变为零,要补一个不携带首任务的 Worker 去取队列,避免有任务却没有消费者;core=0 或最后一个空闲线程退出都与此有关。
实际短源码分支包含 !isRunning(recheck) && remove(command),以及 workerCountOf(recheck) == 0。前者决定可撤回的关闭竞争,后者决定消费能力修复。它们不是单纯“防空指针的双重检查”,也不把整个提交过程变成业务事务。任务是否产生外部效果仍需业务身份、结果查询和幂等协议。
原码:接纳不是一次检查
原码连续摘录(仅统一公共缩进,截取方法局部):ThreadPoolExecutor.java,jdk-17.0.12+7,L1351–L1366。
int c = ctl.get();
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
if (! isRunning(recheck) && remove(command))
reject(command);
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
else if (!addWorker(command, false))
reject(command);
}addWorker 的 true/false 表示按 core/max 限制检查,不是是否后台运行。offer 成功后再次读取 ctl:关闭且能移除才拒绝;移除失败不能断言任务没执行。workerCount 为零时用 null 首任务补消费者,避免队列无人消费。删掉这两支分别会留下关闭竞态与进展缺口。
ctl:运行状态与工作线程数量的联合状态
该标签用 AtomicInteger 保存 ctl,低 29 位为 workerCount,高 3 位为运行状态。实际常量表达式为 COUNT_BITS = Integer.SIZE - 3;数量掩码在该标签名为 COUNT_MASK。RUNNING 编码为负数,其他状态递增,使实现既能拆位,又能直接比较状态范围。一次 CAS 可检查状态和数量是否仍与快照相同,减少“已经停止但按旧数量继续建线程”的窗口。
RUNNING 接受任务并消费队列;SHUTDOWN 不接新任务但继续消费旧队列;STOP 不再正常取队列并尝试中断执行者;TIDYING 说明退出条件已满足,随后执行 terminated 钩子;最后为 TERMINATED。isShutdown 为真不等于所有任务已结束,awaitTermination 才是有时限等待终止的接口。
workerCount 也不等于正在执行业务的线程数:空闲 Worker 仍计数,addWorker 会先预留数量后创建 Thread,因此创建窗口内数量可能先于实际启动。getActiveCount 属于近似监控,不能拿来做严格的“还有一个名额则提交”的检查。容量闸门应在提交/许可协议内原子实施,监控数用来解释负载,不替代接纳判定。
原码:ctl 的位布局
原码连续摘录(仅统一公共缩进):ThreadPoolExecutor.java,jdk-17.0.12+7,L385–L398。
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
private static final int COUNT_BITS = Integer.SIZE - 3;
private static final int COUNT_MASK = (1 << COUNT_BITS) - 1;
// runState is stored in the high-order bits
private static final int RUNNING = -1 << COUNT_BITS;
private static final int SHUTDOWN = 0 << COUNT_BITS;
private static final int STOP = 1 << COUNT_BITS;
private static final int TIDYING = 2 << COUNT_BITS;
private static final int TERMINATED = 3 << COUNT_BITS;
// Packing and unpacking ctl
private static int runStateOf(int c) { return c & ~COUNT_MASK; }
private static int workerCountOf(int c) { return c & COUNT_MASK; }RUNNING 为负数使状态比较可以写成“小于 SHUTDOWN”等范围判断;COUNT_MASK 只剩低 29 位,workerCount 的最大值是 (1<<29)-1。这两个解码函数说明吞吐量字段不是独立变量,任何更新都必须通过一次 CAS 同时维护状态与数量。
addWorker:创建、回退与补位
addWorker 先检查运行状态,再循环检查 core 或 max 界限并 CAS 预留名额。CAS 失败会重读 ctl;状态变化时返回外层重新判断,单纯数量变化可在内层重试。SHUTDOWN 只在仍有排队任务、首任务为空等条件下允许补消费者,不能借补位重新接受一条新业务任务。STOP 之后更严格。
预留名额成功后,Worker 构造器调用 ThreadFactory 创建线程,再持 mainLock 复查状态并登记 workers,随后启动 Thread。工厂可能返回 null,创建或启动可能抛异常,登记前也可能发生关闭。失败时 addWorkerFailed 在 finally 路径移除可能已登记对象、回退计数并检查终止,防止虚假名额永久占据容量。
所以拒绝原因至少包括过载和关闭,线程创建失败还可能以异常形式暴露。线上 reject 增加时,需要同时检查池状态、线程工厂、系统线程或本地内存限制、队列和下游耗时。直接调大 max 无法修复工厂返回 null 或进程无法创建线程的问题,反而可能令资源压力进一步恶化。
Worker 锁与 mainLock 的分工
Worker 继承 AQS,是池控制协议的内部锁。构造时 state=-1,抑制 Worker 正式启动前被控制逻辑中断;runWorker 开始时解锁,之后 0 表示可被识别为空闲,1 表示正在执行任务。执行每个任务时 Worker 锁住自己,结束后释放,让 interruptIdleWorkers 能通过 tryLock 识别空闲线程。
这个锁故意不可重入:任务执行中若线程再调用池控制方法,可重入锁可能让自身被误识别为空闲。mainLock 则保护 Worker 集合与若干池级动作,不会贯穿整个用户任务,否则工作线程就被串行化。Worker 锁不保护两个任务访问的业务 Map,任务共享状态仍要自行同步。
runWorker 先消费 firstTask,之后重复 getTask,依次执行 beforeExecute、task.run、afterExecute,finally 清空 task 引用、增加完成数并解锁。池达到 STOP 时会尽力确保工作线程被中断;普通空闲中断与停止中断不能混淆。任务代码若吞中断或不检查取消,shutdownNow 也无法强制撤销业务副作用。
直接 execute 的 Runnable 抛 RuntimeException/Error,异常可能逃逸 runWorker 导致 Worker 异常退出;processWorkerExit 按池状态与队列补位。submit 则经 AbstractExecutorService.submit/newTaskFor 包装成 FutureTask,用户异常被记录,task.run 通常正常返回。于是 afterExecute 的 Throwable 参数可能为 null,不能据此认为任务成功。
若要在钩子集中监控 submit 异常,应确认 Future 已完成,再读取结果并分别处理 ExecutionException 和 CancellationException,不能在钩子里无条件阻塞等待其他任务。beforeExecute 或 afterExecute 自身抛错也可能结束 Worker;监控代码不能成为新的故障源。完成计数反映执行循环处理过任务,不是业务成功订单数。
原码:任务异常与 Worker 退出
原码连续摘录(仅统一公共缩进,截取方法局部):ThreadPoolExecutor.java,jdk-17.0.12+7,L1134–L1150。
beforeExecute(wt, task);
try {
task.run();
afterExecute(task, null);
} catch (Throwable ex) {
afterExecute(task, ex);
throw ex;
}
} finally {
task = null;
w.completedTasks++;
w.unlock();
}
}
completedAbruptly = false;
} finally {
processWorkerExit(w, completedAbruptly);task.run 正常返回时 afterExecute 参数为 null;逃逸 Throwable 时先调用异常钩子再继续抛出。finally 清空任务引用、累计处理数并释放 Worker 锁,外层再 processWorkerExit。FutureTask 把业务异常保存为结果时,task.run 可正常返回,因此不能只看钩子的 Throwable 来判断 submit 业务成功。
getTask、队列和回收
getTask 根据 allowCoreThreadTimeOut 或当前数量超过 core 决定用带时限 poll 还是 take。超时后退出前需再次检查数量和队列并 CAS 减计数,避免多个线程都按旧快照退出;空闲等待被中断时会回到循环检查状态。开启核心超时要满足 keepAliveTime 为正等接口条件,不能把零等待误写成正常配置。
ArrayBlockingQueue 给出固定容量;LinkedBlockingQueue 必须明确容量才有可操作上限。SynchronousQueue 不保存任务,需要与消费者交接,交接失败会更早走扩线程或拒绝路径,不是“容量为一”的队列。PriorityBlockingQueue 改变取出顺序且通常还要解决容量,所以 DiscardOldest 在它上面也不再意味着丢弃提交时间最早的请求。BlockingQueue 契约 还区分 put、offer 和带时限 offer,不能把 execute 说成阻塞式入队。
keepAlive 控制空闲资源,不是任务超时。任务执行 10 分钟时,不能用 keepAlive=30 秒推导它会被打断;业务 deadline 需要单独传递和检查。取消一个排队 Future 也未必立即移除其队列节点;可按场景考虑 remove/purge,但不要把维护队列的开销隐含成零成本。PoolAsyncProbe 第 4 节实测三条边界:allowCoreThreadTimeOut(true)(要求 keepAlive 为正)后空闲池可缩到 0;不开该开关时 core=1 的空闲线程在超过 keepAlive 的窗口后仍保留;keepAlive=100ms 不打断一个执行 400ms 的任务。
原码:getTask 的超时与关闭分支
原码连续摘录(仅统一公共缩进,截取完整方法):ThreadPoolExecutor.java,jdk-17.0.12+7,L1034–L1070。
private Runnable getTask() {
boolean timedOut = false; // Did the last poll() time out?
for (;;) {
int c = ctl.get();
// Check if queue empty only if necessary.
if (runStateAtLeast(c, SHUTDOWN)
&& (runStateAtLeast(c, STOP) || workQueue.isEmpty())) {
decrementWorkerCount();
return null;
}
int wc = workerCountOf(c);
// Are workers subject to culling?
boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
if ((wc > maximumPoolSize || (timed && timedOut))
&& (wc > 1 || workQueue.isEmpty())) {
if (compareAndDecrementWorkerCount(c))
return null;
continue;
}
try {
Runnable r = timed ?
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
workQueue.take();
if (r != null)
return r;
timedOut = true;
} catch (InterruptedException retry) {
timedOut = false;
}
}
}关闭检查只在 SHUTDOWN 且队列为空、或已到 STOP 时退线程;超时退出必须同时满足数量条件并用 CAS 减计数。若多个线程都看到旧 wc 并直接退出,池可能失去消费者,因此失败要 continue 重读。中断只把 timedOut 清为 false 并重试,不把池控制中断等同于业务取消。
四种拒绝策略与 Future 永不完成
AbortPolicy 显式抛 RejectedExecutionException,调用方容易识别过载,但仍需决定如何对用户响应。CallerRunsPolicy 在池未关闭时让提交者直接执行,能降低提交速度,却可能把阻塞传播到 HTTP 线程、事件循环或持锁调用者;池关闭后它也可能忽略任务,不能统一解释成“一定执行”。
DiscardPolicy 静默丢弃;DiscardOldestPolicy 先取队首再重试提交。如果被丢弃的是 submit 创建的 FutureTask,丢弃不自动完成或取消 Future,等待者可能永远挂起。换句话说,“没抛拒绝异常”和“拿到了 Future”都不能证明异步结果最终可达。四种策略实现 在文件末尾,可直接检查各分支是否设置 Future 终态。
原码:CallerRuns 的两个分支
原码连续摘录(仅统一公共缩进,截取方法局部):ThreadPoolExecutor.java,jdk-17.0.12+7,L2035–L2039。
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
r.run();
}
}未关闭时 r.run() 在调用者线程同步执行——提交被反压,池的并行度没有变大;已关闭时整个方法静默返回,任务既没执行也没抛异常、没有任何 Future 终态。触发条件要区分两件事:过载(core 满、队列满、max 满)走 reject;关闭竞争(SHUTDOWN 后新任务)同样走 reject。前者是资源边界,后者是生命周期边界,CallerRuns 在两个分支里的行为完全不同(PoolAsyncProbe 第 3 节实测两个分支)。DiscardOldest 的“丢最老”以队列为谁:PriorityBlockingQueue 上丢的是比较序最前的元素,不一定是提交时间最早的请求。
自定义拒绝处理器应定义被拒任务的终态、指标、告警及恢复者。如果取消 Future,说明这是调用方结果协议,不是事务回滚。不要在满池里无限递归重投,也不要阻塞入队却不给上游剩余预算。可靠任务需要任务表、可靠消息或调用方重试约定,不能靠加长内存队列声称“任务不丢”。
shutdown 与 shutdownNow:两种关闭协议
shutdown() 的语义是“停止接纳、继续收尾”:状态推进到 SHUTDOWN,只中断空闲 Worker,队列中的任务继续被消费。shutdownNow() 的语义是“停止一切、搬走队列”:状态推进到 STOP,中断所有 Worker(无论是否在执行任务),并把队列中未开始的任务 drainQueue 出来返回给调用方。两者都不等待:awaitTermination(timeout, unit) 才是有时限的等待终止接口,isShutdown() 为真不代表任务结束,isTerminated() 才代表全部 Worker 退出。
原码:shutdown 只打断空闲
原码连续摘录(仅统一公共缩进,截取方法局部):ThreadPoolExecutor.java,jdk-17.0.12+7,L1379–L1391。
public void shutdown() {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
checkShutdownAccess();
advanceRunState(SHUTDOWN);
interruptIdleWorkers();
onShutdown(); // hook for ScheduledThreadPoolExecutor
} finally {
mainLock.unlock();
}
tryTerminate();
}advanceRunState(SHUTDOWN) 之后新任务按关闭竞争拒绝,已入队任务保留。interruptIdleWorkers() 为什么能只打断空闲:Worker 执行任务时锁住自己(Worker 锁 state=1),tryLock 失败即跳过,所以正在执行的任务不会收到中断;空闲 Worker 在 getTask 的 poll/take 里阻塞、锁空闲,tryLock 成功才 interrupt——这个中断被 getTask 捕获后只是重读状态,队列非空时继续消费。onShutdown 是 ScheduledThreadPoolExecutor 的钩子,普通池为空操作。
原码:shutdownNow 全量中断并搬走队列
原码连续摘录(仅统一公共缩进,截取方法局部):ThreadPoolExecutor.java,jdk-17.0.12+7,L1410–L1424。
public List<Runnable> shutdownNow() {
List<Runnable> tasks;
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
checkShutdownAccess();
advanceRunState(STOP);
interruptWorkers();
tasks = drainQueue();
} finally {
mainLock.unlock();
}
tryTerminate();
return tasks;
}interruptWorkers() 不做 tryLock 检查,所有 Worker 一律 interrupt——执行中的任务是否退出取决于任务是否响应中断(中断协议);不响应的任务会继续运行到自行结束,池在此期间无法 TERMINATED。drainQueue 把队列元素搬出来返回,这些 Runnable 没有 Future 终态、没有业务身份,调用方要自己决定重跑、登记或丢弃。返回列表只含“没有开始执行”的队列元素:已经被 Worker 取走的任务不在其中,但可能已产生外部副作用——“返回列表为空”不能证明业务全部落停。
原码:interruptIdleWorkers 的 tryLock 门
原码连续摘录(仅统一公共缩进,截取方法局部):ThreadPoolExecutor.java,jdk-17.0.12+7,L791–L811。
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()) {
try {
t.interrupt();
} catch (SecurityException ignore) {
} finally {
w.unlock();
}
}
if (onlyOne)
break;
}
} finally {
mainLock.unlock();
}
}三个细节值得读出:!t.isInterrupted() 避免重复中断已收到信号的线程;w.tryLock() 是“空闲判定”,任务执行中锁被持有、tryLock 失败即跳过;onlyOne 供 ensurePrestart/超时路径使用——注释说明“只中断一个也能保证最终终止”,shutdown() 全量打断只是让多余 Worker 尽快退出。PoolAsyncProbe 第 1 节实测:shutdown() 后队列任务照常执行、新任务被拒;shutdownNow() 后队列任务被搬走、正在执行且等待闩锁的任务经 InterruptedException 协作退出;完全不检查中断的任务在 shutdownNow() 后仍存活,直到自己的标志位翻转。
优雅停机因此至少四步:停止新接纳(shutdown)、等待有界时间(awaitTermination 带超时)、处理未开始任务(shutdownNow 返回列表或队列快照逐个处置)、协调在途副作用(任务身份、幂等键、查单/补偿)。只在 finally 里 shutdown 然后进程退出,等于把“可能没做完”交给运气。
同池依赖死锁与关闭时的任务所有权
考虑一个固定大小为 2 的池:两个父任务同时开始,各向同一池 submit 一个子任务,然后 get 等待。子任务排在队列里,但所有 Worker 都被等待中的父任务占据。CPU 可能很低,队列持续有任务,却没有线程能执行它们。这是任务依赖与执行资源形成的等待环,不要求出现 Java 监视器死锁,所以线程转储的死锁检测未必报告锁环。
把队列调大只会容纳更多子任务,不能打破资源等待。候选方案包括不阻塞地组合结果、把父子阶段安排到能独立推进的执行器,或在提交前明确并发与依赖层数上限。拆两个池也需要证明不会形成反向调用环,不能仅凭“池隔离了”宣布消除死锁。带时限 get 能让等待者放弃等待,但若不取消与清理子任务,超时后队列仍可能继续消耗资源。
shutdownNow 返回的 Runnable 列表表示没有开始执行的队列元素,不会替每个 Future 设终态。调用方若持有这些任务的 Future,需要按关闭协议取消或登记重试;正在执行的任务可能已发送远端请求,不能和返回列表中的任务用同一条“安全重做”规则处理。优雅停机应停止新接纳、等待有界时间、处理未开始任务、协调在途副作用,并留下可追踪任务身份。这比仅在 finally 调用 shutdown 更接近可恢复的服务退出。
有输入的实验:低负载、过载与失败分支
可运行入口为 Java 高级实验 中的 poolasync 模式(U09 起本页实验主入口;pool 模式保留 SeniorProbe 历史断言):
JAVA_HOME=/Library/Java/JavaVirtualMachines/jdk-17.jdk/Contents/Home \
bash labs/java-senior/run.sh poolasync与本页直接相关的三节本轮实际输出(节选):
== 1. shutdown vs shutdownNow: queue draining and interruption scope ==
shutdown(): new task rejected, but queued task still executed
shutdownNow(): returned 1 queued task (never executed); running task exited via InterruptedException
shutdownNow() on interrupt-blind task: worker alive after 300ms=true(观察值)
== 2. task exception: execute vs submit, afterExecute observation ==
execute(throws): afterExecute received "execute-boom"; worker thread exits-1 died
replacement worker for follow-up task: exits-1 -> exits-2(逃逸异常导致 Worker 重建)
submit(throws): afterExecute received null; Future.get -> ExecutionException("submit-boom"); worker exits-2 retained
== 3. rejection: CallerRuns both branches and DiscardOldest head-drop ==
CallerRunsPolicy while RUNNING: third task executed on caller thread "main"
CallerRunsPolicy after shutdown(): task silently discarded, no exception thrown
DiscardOldestPolicy: queue head B dropped, newcomer D accepted; order=[A, C, D]第 1 节用闩锁钉住“正在执行 + 已排队”的交错:shutdown() 后新提交被拒,但排队任务在闩锁释放后照常执行;shutdownNow() 返回的列表恰好是那 1 个排队任务且从未执行,执行中任务经 InterruptedException 协作退出;第三组完全不检查中断的任务在 shutdownNow() 300ms 后仍存活,直到自己的标志位翻转——这条是观察值,API 只承诺尽力中断。
第 2 节对照两条异常出口:execute 的任务抛错,异常穿过 runWorker 打到线程的未捕获处理器(stderr 出现 IllegalStateException: execute-boom,这是实验的一部分不是故障),afterExecute 收到该异常,Worker 死亡并由新线程 exits-2 重建;submit 的同款任务抛错时 stderr 无输出、钩子收到 null、线程保留,唯一暴露点是 Future.get 的 ExecutionException。
第 3 节覆盖 CallerRuns 两个分支与 DiscardOldest 丢队首:A 阻塞、B/C 排队后提交 D,策略取走队首 B 再重试 D,最终执行序 A→C→D。接纳顺序的 core=1/max=2/queue=1 四任务断言(第四任务拒绝)与 DiscardPolicy 留下未完成 Future 的断言由 SeniorProbe pool 模式历史输出承载:
PASS core=1 max=2 queue=1: fourth task rejected
PASS DiscardPolicy leaves submitted Future unfinished纸面推演仍保留:T1 先读 RUNNING;T2 执行 shutdown;T1 入队成功后复查。分别填出 remove 成功、任务已被取走、Worker 计数为零时的动作。记录任务当前属于提交者、队列还是执行者;结果必须和 execute 真实分支一致,不用睡眠碰运气“证明”竞态。
这些输入验证接纳、关闭与结果终态,不是吞吐基准。运行状态和原始输出由实验目录记录,没运行时以上只是预期;一次成功也不能证明任意调度下没有任务重复或业务错误。
竞争时间线:关闭、入队与补位
| 时间 | 提交线程 T1 | 关闭线程 T2 | ctl / 队列 / 任务归属 |
|---|---|---|---|
| t1 | 读到 RUNNING,workerCount=1 | — | 任务尚未入队 |
| t2 | — | shutdown() | 状态 SHUTDOWN,队列保留 |
| t3 | offer 成功 | — | 任务在队列,可能无人消费 |
| t4 | 复查发现非 RUNNING | — | remove 成功则拒绝 |
| t5 | remove 失败 | — | 任务可能已被 Worker 取走,不能断言未执行 |
| 场景 | 复查条件 | 动作 | 后果 |
|---|---|---|---|
| 关闭且可撤回 | !isRunning && remove | reject | 任务明确未入执行 |
| 关闭但已被取走 | !isRunning && !remove | 不拒绝 | 任务已进入执行者 |
| 计数归零 | workerCountOf(recheck)==0 | addWorker(null,false) | 队列重新有消费者 |
时间线只说明协议分支,不证明业务幂等;任务被取走与业务成功仍是两件事。
容量计算与条件变化
假设到达率 200 任务/s,每个任务平均占线程 50ms,其中 CPU 工作 5ms,下游最多允许 16 个在途。平均执行需求约为 200×0.05=10 个线程;这只是稳定负载平均值,不保证 10 线程满足 P99。配置 12 线程时,理想服务能力约 240/s,突发与长尾仍会排队。
若排队预算 100ms,以 240/s 服务能力粗估,24 个等待任务已经对应一个预算量级。这不是严格等待上界,必须结合耗时分布。若每个任务保留 256KiB 对象,1000 个队列元素仅负载就约 250MiB,尚未计算节点和引用。容量必须同时约束时间和内存,不能仅因堆够大就无限延长等待。
条件变成下游只允许 8 路时,扩线程不能提高下游服务能力,应限制在途、快速失败或降级;把工作持久化排队则需要定义延迟、重复执行和恢复责任。条件变成任务不能丢时,拒绝后的可靠存储或调用方重试是协议的一部分。条件变成读请求允许部分结果时,可按依赖重要度分配独立预算和池,避免非关键依赖占满关键路径。
恢复应先修复慢依赖或减少流量,再处理最老排队任务与已过期任务,逐步放量。直接翻倍 max 可能同时压垮连接池、远端与 GC。验证至少包括到达率、完成率、队列年龄、拒绝/超时比例、下游在途和恢复后的重复业务记录;日志中的“已提交任务数”不等于已成功业务数。
三轮递进追问
第一轮:调大 max 为什么没用?先按 execute 查看队列是否能不断 offer,再检查 core、工厂失败与下游瓶颈。评分点:使用分支定位,不能仅背线程数公式。
第二轮:换 SynchronousQueue 就没有等待了吗?等待可能转移到线程、连接池或远端,必须配合线程上限、拒绝与 deadline。评分点:交接语义、压力转移、端到端预算。
第三轮:支付任务取消后可以安全重试吗?Future 取消不回滚外部效果,中断依赖协作,远端可能已提交。需要稳定业务键、查单与对账;线程池只限制资源并报告局部状态。评分点:三种终态区分与恢复责任。
闭卷验收
本页 14 道闭卷题收进了下方自测面板:先回忆,再展开参考答案与评分点,最后给三档自评。
来源与版本
| 来源 | 版本 | 用途 |
|---|---|---|
| Java SE 17 ThreadPoolExecutor API | Java SE 17 | 核心/最大线程数、队列、拒绝与关闭语义 |
| Java SE 17 ExecutorService API | Java SE 17 | shutdown/shutdownNow/awaitTermination 契约 |
| Java SE 17 BlockingQueue API | Java SE 17 | put/offer/带时限 offer 的差异 |
| OpenJDK 17u ThreadPoolExecutor | jdk-17.0.12+7 | ctl 位布局(L385–L398)、interruptIdleWorkers(L791–L811)、getTask(L1034–L1070)、任务异常(L1134–L1150)、execute(L1351–L1366)、shutdown(L1379–L1390)、shutdownNow(L1410–L1424)、CallerRunsPolicy.rejectedExecution(L2035–L2039) |
| OpenJDK 17u AbstractExecutorService | jdk-17.0.12+7 | submit 到 FutureTask 的包装 |
本地 PoolAsyncProbe poolasync | Oracle JDK 17.0.12 | 关闭语义、任务异常出口、CallerRuns 两分支、DiscardOldest 丢队首、keepAlive/allowCoreThreadTimeOut |
本地 SeniorProbe pool | Oracle JDK 17.0.12 | 核心/最大/队列接纳与 DiscardPolicy 未完成终态(历史断言) |
访问日期:2026-10-25(U09 复核;初版 2026-10-23)。ctl 编码、Worker 锁与 getTask 分支属于固定版本实现,不是 API 契约;升级 JDK 需重新核对。