线程池复用依赖runWorker的死循环,但未捕获异常会导致被动崩溃退出;execute提交任务时异常直接终止线程,submit则封装为FutureTask捕获异常;异常退出后线程池会补新Worker,频繁发生将增加CPU和GC压力。

runWorker 里的死循环是复用的前提,但也是异常退出的入口
线程池能复用线程,靠的是 runWorker 方法里那个不设出口的 while (task != null || (task = getTask()) != null) 循环。只要循环不退出,线程就一直活着、一直取任务。但这个循环本身没有 try-catch 包裹用户任务——task.run() 一旦抛出未捕获异常(比如 NullPointerException),就会直接跳出循环体,执行后续的 finally 块,最终进入 processWorkerExit 流程。
关键点在于:这个退出不是“主动释放”,而是“被动崩溃”。你看到线程没了,其实是它被异常顶出了循环,根本没机会走到下一次 getTask()。
execute 和 submit 对异常的处理路径完全不同
用 execute 提交任务时,异常会原路穿透到 runWorker 的 task.run() 调用点,触发线程终止;而用 submit 提交,任务会被包装成 FutureTask,其 run 方法内部有 try-catch,异常被捕获并存入 outcome 字段,不会中断循环。
-
execute(() -> { throw new RuntimeException(); })→ 线程立即退出,日志可能只在System.err输出一行堆栈(若没设UncaughtExceptionHandler) -
submit(() -> { throw new RuntimeException(); })→ 线程继续存活,异常要等你调用future.get()才抛给调用方线程
线程退出后,ThreadPoolExecutor 会悄悄补一个新 Worker
异常退出的 Worker 会被 processWorkerExit(w, true) 移除,参数 completedAbruptly == true 表示非正常结束。此时线程池会检查当前运行中的线程数是否低于 corePoolSize(或 allowCoreThreadTimeOut 启用时的动态阈值),如果低于,就调用 addWorker(null, false) 新建一个 Worker 补位。
这意味着:单次异常看似“无感”,但频繁发生会导致线程反复销毁/重建,CPU 和 GC 压力上升,且旧线程持有的局部变量、TLS(ThreadLocal)等资源无法复用。
容易被忽略的 InterruptedException 处理陷阱
当任务里调用了 Thread.sleep()、queue.take() 等可中断方法,并被线程池 shutdownNow() 中断时,会抛 InterruptedException。如果只写 catch (InterruptedException e) { } 而没调用 Thread.currentThread().interrupt(),中断状态丢失,后续逻辑可能误判为“任务完成”,导致线程提前退出循环——这种退出没有异常堆栈,极难定位。
更隐蔽的是:某些阻塞队列(如 LinkedBlockingQueue)的 poll(timeout) 在超时后返回 null,也会让 getTask() 返回 null,从而自然退出循环。这属于设计行为,但和异常退出混在一起时,日志里看不出区别。










