线程池事故与JUC源码速查#
一句话答案#
线程池事故三大类:拒绝策略不当(CallerRunsPolicy 阻塞主线程)、队列无界(OOM)、线程泄漏(异常未捕获导致线程消亡);JUC 源码追问集中在 AQS 的 state+CLH 队列、ReentrantLock 的公平/非公平、ThreadPoolExecutor 的 ctl 状态位设计。
核心要点
一、线程池事故案例集
案例1:无界队列 OOM
- 现象:服务内存持续增长,最终 OOM
- 原因:
Executors.newFixedThreadPool()默认LinkedBlockingQueue无界,任务积压 - 排查:jmap dump → 发现队列中数万个 Runnable 对象
- 修复:换成有界队列 + 合理拒绝策略;阿里规范禁止用 Executors 快捷方法
案例2:CallerRunsPolicy 阻塞主线程
- 现象:接口偶发超时,且超时时间=任务执行时间
- 原因:线程池满+队列满→CallerRunsPolicy 让提交线程(Tomcat线程)执行任务
- 排查:jstack 发现 Tomcat 线程在执行业务逻辑而非等待
- 修复:改用自定义拒绝策略(记录日志+降级)或增大队列/核心线程
案例3:线程泄漏(线程消亡不补充)
- 现象:线程池 active 逐渐降为 0,任务无人执行
- 原因:任务抛出未捕获异常,execute() 会导致线程消亡;submit() 异常在 Future 中
- 排查:
pool.getActiveCount()为 0 + 日志无异常(被吞了) - 修复:任务内 try-catch +
afterExecutehook 记录异常 + UncaughtExceptionHandler
案例4:线程池参数配不对导致延迟
- 现象:异步任务延迟高(排队时间长)
- 原因:核心线程数=2 + 队列=1000,任务先堆队列再扩线程,延迟=队列排队时间
- 关键理解:
核心满 → 入队 → 队列满 → 扩到最大 → 满了 → 拒绝 - 修复:IO 密集型调大核心线程数(2*CPU)、用 SynchronousQueue 让任务直接匹配线程
二、线程池参数调优指南
| 场景 | core | max | queue | 策略 |
|---|---|---|---|---|
| CPU密集型 | CPU核数+1 | =core | 小队列(64-256) | AbortPolicy |
| IO密集型 | 2*CPU | 4*CPU | 有界(500-2000) | 自定义降级 |
| 混合型 | 按IO占比估算 | 2*core | 有界 | 监控动态调整 |
动态调优:美团线程池实践 → 配置中心动态修改 core/max/queue → setCorePoolSize() 热生效
三、AQS 源码核心
AQS 核心结构:
┌─ volatile int state (锁状态:0=空闲,>0=被持有,重入时累加)
├─ Thread exclusiveOwnerThread (当前持锁线程)
└─ CLH 双向队列 (等待获取锁的线程节点)
head ↔ Node ↔ Node ↔ tailplaintextacquire 流程(独占模式):
tryAcquire(1)→ CAS 尝试将 state 0→1- 成功 → 设置 exclusiveOwnerThread = currentThread
- 失败 →
addWaiter()入队 CLH →acquireQueued()自旋/park - 自旋条件:前驱是 head 时再尝试 tryAcquire
- 非 head 前驱 →
LockSupport.park()阻塞
release 流程:
tryRelease(1)→ state - 1,state=0 时真正释放unparkSuccessor(head)→ 唤醒 CLH 队列中下一个等待节点
公平 vs 非公平锁(ReentrantLock):
- 非公平(默认):
tryAcquire时先 CAS 抢锁,抢不到再排队 - 公平:
tryAcquire先检查hasQueuedPredecessors(),有人排队就不抢
四、ThreadPoolExecutor ctl 状态位设计
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
// 高3位:线程池状态 低29位:工作线程数
// RUNNING = 111... (接受新任务+处理队列任务)
// SHUTDOWN = 000... (不接新任务+处理队列)
// STOP = 001... (不接+不处理+中断进行中)
// TIDYING = 010... (所有任务完成,即将terminated)
// TERMINATED = 011... (terminated完成)java为什么用一个 AtomicInteger 而非两个变量?→ 一次 CAS 同时更新状态和线程数,避免不一致。
五、关键 JUC 类源码入口
| 类 | 核心方法 | 看什么 |
|---|---|---|
| ReentrantLock | lock()/tryAcquire() | 公平/非公平分支 |
| CountDownLatch | await()/countDown() | state=count,countDown CAS减1,await时state≠0就park |
| Semaphore | acquire()/release() | state=permits,acquire 减,release 加 |
| CyclicBarrier | await() | ReentrantLock+Condition,count减到0时signalAll |
| CompletableFuture | thenApply()/join() | 栈式回调链+CAS完成态 |
面试回答(2分钟版)
线程池事故我总结三大类。第一类是无界队列 OOM:Executors.newFixedThreadPool 默认用无界的 LinkedBlockingQueue,流量突增时任务无限堆积撑爆内存。第二类是 CallerRunsPolicy 反噬:线程池满时让提交线程执行任务,Tomcat 线程变成了业务线程导致接口超时。第三类是线程消亡不补充:execute 提交的任务抛异常后线程直接死掉,而且异常不打日志很难排查。修复分别是:用有界队列加自定义拒绝策略、合理设置核心线程数、任务内 try-catch 加 UncaughtExceptionHandler。JUC 源码核心是 AQS:一个 volatile state 变量加一个 CLH 双向等待队列。获取锁时 CAS 改 state,改成功就持有,改不成功入队自旋或 park。释放锁时 state 减到 0,unpark 队列中下一个节点。ReentrantLock 公平锁多了一步 hasQueuedPredecessors 检查,有人排队就不插队。ThreadPoolExecutor 用一个 AtomicInteger 的高3位存状态低29位存线程数,一次 CAS 同时更新两个信息。
追问与易错
追问方向:
- “execute() 和 submit() 异常处理有什么区别?”→ execute 抛异常线程消亡;submit 异常封装在 Future.get() 里
- “怎么监控线程池健康?”→ getActiveCount/getQueueSize/getCompletedTaskCount + Prometheus 暴露
- “AQS 为什么用 CLH 队列不用普通队列?”→ CLH 自旋在前驱节点上,减少全局竞争
- “为什么 AQS 用 park 不用 wait?”→ park 不需要持有锁,可以先 unpark 再 park 不会丢失唤醒
易错点:
- ❌ “线程池越大越好”——CPU 密集型核心数+1 就够,多了反而上下文切换大
- ❌ “submit 提交的任务异常会打印日志”——不会,必须 Future.get() 才能看到
- ❌ “公平锁性能和非公平一样”——公平锁吞吐量低很多(每次都要检查队列)
- ✅ 核心记忆:execute 流程是 core→queue→max→reject,不是 core→max→queue→reject