面试知识库
困难

线程池事故与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 + afterExecute hook 记录异常 + UncaughtExceptionHandler

案例4:线程池参数配不对导致延迟

  • 现象:异步任务延迟高(排队时间长)
  • 原因:核心线程数=2 + 队列=1000,任务先堆队列再扩线程,延迟=队列排队时间
  • 关键理解:核心满 → 入队 → 队列满 → 扩到最大 → 满了 → 拒绝
  • 修复:IO 密集型调大核心线程数(2*CPU)、用 SynchronousQueue 让任务直接匹配线程

二、线程池参数调优指南

场景coremaxqueue策略
CPU密集型CPU核数+1=core小队列(64-256)AbortPolicy
IO密集型2*CPU4*CPU有界(500-2000)自定义降级
混合型按IO占比估算2*core有界监控动态调整

动态调优:美团线程池实践 → 配置中心动态修改 core/max/queue → setCorePoolSize() 热生效

三、AQS 源码核心

AQS 核心结构:
  ┌─ volatile int state (锁状态:0=空闲,>0=被持有,重入时累加)
  ├─ Thread exclusiveOwnerThread (当前持锁线程)
  └─ CLH 双向队列 (等待获取锁的线程节点)
       head ↔ Node ↔ Node ↔ tail
plaintext

acquire 流程(独占模式):

  1. tryAcquire(1) → CAS 尝试将 state 0→1
  2. 成功 → 设置 exclusiveOwnerThread = currentThread
  3. 失败 → addWaiter() 入队 CLH → acquireQueued() 自旋/park
  4. 自旋条件:前驱是 head 时再尝试 tryAcquire
  5. 非 head 前驱 → LockSupport.park() 阻塞

release 流程:

  1. tryRelease(1) → state - 1,state=0 时真正释放
  2. 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 类源码入口

核心方法看什么
ReentrantLocklock()/tryAcquire()公平/非公平分支
CountDownLatchawait()/countDown()state=count,countDown CAS减1,await时state≠0就park
Semaphoreacquire()/release()state=permits,acquire 减,release 加
CyclicBarrierawait()ReentrantLock+Condition,count减到0时signalAll
CompletableFuturethenApply()/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