线程池 · 锁与AQS · volatile/CAS · ThreadLocal · 同步工具
new Thread(task).start()——来一个请求就创建一个线程。
结果 QPS 到 5000 时,JVM 里创建了 3 万多个线程,
每个线程默认占 1MB 栈空间,直接吃掉 30GB 内存(服务器才 16GB)。
更致命的是:3 万个线程在 CPU 只有 8 核的机器上来回切换,
光切换开销就吃掉了 90% 的 CPU,业务代码几乎没机会执行。
线程池 = 餐厅后厨。想象一家餐厅的后厨:
• 核心线程 = 正式厨师(常驻员工,没活也待命)
• 临时线程 = 旺季临时工(忙不过来才招,闲了就辞退)
• 工作队列 = 订单排队区(厨师都在忙,新订单先排队)
• 拒绝策略 = 餐厅满座时的处理方式(直接拒绝?让客人自己来帮忙?丢掉最早的订单?)
没有"后厨管理"的餐厅,每来一单就临时招一个厨师,做完就辞退——招人成本极高,厨房爆满混乱。
有了线程池,厨师固定、排队有序、超载有预案,整个系统才能稳定运转。
线程池的三个核心价值:①控制线程数量(防止 OOM)②队列缓冲(削峰填谷)③拒绝策略(超载保护)。
对照"餐厅后厨"理解这 7 个参数:
① corePoolSize = 正式厨师数量 ② maximumPoolSize = 正式+临时厨师上限
③ keepAliveTime = 临时工空闲多久辞退 ④ unit = 时间单位
⑤ workQueue = 订单排队区大小 ⑥ threadFactory = 给厨师发工牌(命名线程)
⑦ handler = 满座时的处理策略
构造方法签名:
| 参数 | 技术含义 | 生产配置要点 |
|---|---|---|
| corePoolSize | 核心线程数,即使空闲也不回收(除非设 allowCoreThreadTimeOut) | 常驻线程,CPU密集型=核数+1,IO密集型=核数×2 |
| maximumPoolSize | 线程池最大线程数 = 核心 + 临时 | 峰值期才创建临时线程,不可超过此上限 |
| keepAliveTime | 临时线程空闲超过此时间就被回收 | 通常设 60 秒,避免临时线程长期占用资源 |
| unit | keepAliveTime 的时间单位 | 秒 / 分钟 / 毫秒 |
| workQueue | 等待执行的任务阻塞队列 | 必须有界!无界队列任务堆积会 OOM |
| threadFactory | 创建线程的工厂,可自定义线程名 | 自定义命名(如 order-1),方便 jstack 排查 |
| handler | 队列满且线程全忙时的拒绝策略 | 生产环境不要用默认 AbortPolicy |
任务提交后依次判断:核心线程是否还有空闲 → 没有则放入工作队列 → 队列满了才创建临时线程(到 maximumPoolSize)→ 临时线程也满了就触发拒绝策略。注意顺序是先入队列再扩线程,不是先创建到最大线程数再入队列。
面试官常问:"线程池是先扩线程还是先入队列?" 答案是 先入队列,再扩线程。这和很多人直觉相反!核心线程满了之后,任务先进队列,队列满了才会创建临时线程到 maximumPoolSize。这个设计的原因是:创建线程的开销远大于入队,优先用队列缓冲。
当核心线程 + 队列 + 最大线程全部满载时,新来的任务怎么处理?
做法:直接抛出 RejectedExecutionException
效果:调用方收到异常,需要自己 try-catch 处理。如果上层没有处理,可能导致整个请求失败。
做法:由提交任务的线程自己执行该任务
效果:提交线程被阻塞去执行任务,相当于自动限流——上游看到提交变慢,自然就放慢了提交速率。
做法:默默丢弃任务,不抛异常
效果:任务静默消失,没有任何日志。生产环境几乎不用——出了问题完全无法排查。
做法:丢弃队列中最老的任务,然后重新提交当前任务
效果:牺牲最早的任务来容纳最新任务。适合过时数据无意义的场景(如实时行情推送)。
生产环境中不要用默认的 AbortPolicy,因为直接抛异常可能导致整个服务不可用。推荐使用 CallerRunsPolicy 或自定义策略(如记录日志+入库重试)。
| 任务类型 | 公式 | 原理 |
|---|---|---|
| CPU 密集型 (计算、加密、压缩) |
CPU核数 + 1 | 线程多了反而增加上下文切换开销。+1 是为了在某个线程偶尔因缺页中断暂停时,额外线程能补上 |
| IO 密集型 (网络请求、DB查询、文件读写) |
CPU核数 × 2 或 CPU核数 / (1-阻塞比) |
线程大部分时间在等IO,CPU空闲,可以多配线程充分利用CPU。阻塞比=0.9 → 线程数=核数×10 |
| 混合型 | 先按IO密集配,再压测调优 | 实际业务都是混合型,先用大一点的值,看 CPU 利用率和队列长度再调 |
CPU密集型为什么线程数不能多?因为线程都在做计算,CPU已经满载了,多加线程只会增加上下文切换的开销(保存/恢复寄存器、TLB刷新等),反而降低总吞吐量。IO密集型为什么可以多?因为线程大部分时间在阻塞等待IO响应(网络、磁盘),CPU是空闲的,多配线程可以让CPU在等待期间处理其他任务。
面试中说出"线程数不是越多越好"和"需要根据 CPU 核数和任务类型来定"就能拿到基本分。能说出具体公式 + 动态调整(如美团动态线程池)是加分项。
核心=最大=5,队列=LinkedBlockingQueue(无界!)
风险:任务堆积 → 队列无限增长 → OOM
核心=最大=1,队列=LinkedBlockingQueue(无界!)
风险:同上,无界队列 OOM
核心=0,最大=Integer.MAX_VALUE,队列=SynchronousQueue
风险:来多少任务创建多少线程 → 线程爆炸 → OOM
核心=5,最大=Integer.MAX_VALUE,延迟/定时任务
风险:同样可能线程爆炸
阿里巴巴 Java 开发手册明确规定:线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式。原因就是上面的 OOM 风险。面试说出这个规范非常加分。
面试官问完线程池,90% 会追问锁和 AQS——这是并发编程的核心骨架。
| 对比维度 | synchronized | ReentrantLock |
|---|---|---|
| 实现层面 | JVM 层面(monitorenter/monitorexit 字节码) | API 层面(java.util.concurrent.locks) |
| 锁的获取 | 自动获取,退出同步块自动释放 | 手动 lock() / unlock(),必须在 finally 中释放 |
| 可中断 | 不可中断,等锁时无法响应中断 | lockInterruptibly() 可响应中断 |
| 公平性 | 非公平锁 | 可选公平/非公平(构造参数) |
| 条件变量 | 只能 wait/notify(一个条件) | 多个 Condition,精准唤醒 |
| 尝试获取 | 不能尝试,阻塞等待 | tryLock(),获取不到立即返回 |
| 锁升级 | 偏向锁 → 轻量级锁 → 重量级锁(JVM 自动优化) | 无锁升级机制,始终依赖 AQS |
| 使用场景 | 简单同步,锁竞争不激烈 | 需要公平锁、可中断、多条件、超时等高级功能 |
synchronized = 公共厕所的门锁:进了就锁,出来就开,没得商量——不需要手动释放,不能超时,不能中断。ReentrantLock = 银行取号排队系统:可以设 VIP 优先(公平锁)、可以超时放弃(tryLock)、可以只叫某一类号(多 Condition 精准唤醒)——功能强大,但要自己记得 unlock。
AQS 是 java.util.concurrent 的基石,ReentrantLock、Semaphore、CountDownLatch 都基于它。
AQS = 银行取号排队系统。
• state = 柜台状态(0=空闲,1=有人正在办理)
• CAS 抢锁 = 进门先看柜台空不空,空就冲过去(CAS),被别人抢先了就放弃
• CLH 队列 = 取号排队区,按 FIFO 先来先服务,每个人坐在椅子上等
• park/unpark = 坐着睡觉(park 挂起,不消耗 CPU)/ 叫号时被拍醒(unpark 唤醒)
• 模板方法 = 银行规定了排队叫号的流程(AQS),但每个窗口"怎么办理业务"由各个银行自己定(ReentrantLock、Semaphore 等子类)
关键理解:
• state:锁的状态。独占锁 0/1,共享锁可 >1(如 Semaphore 允许多个许可)
• CLH 队列:双向链表,FIFO 排队。每个节点记录等待状态(SIGNAL/CANCELLED 等)
• 模板方法模式:AQS 定义排队/阻塞逻辑,子类只实现 tryAcquire/tryRelease(怎么抢锁/怎么释放)
锁只能升级,不能降级(GC 时可以)。面试中说出这四个阶段 + 只升不降就够了。
为什么要锁升级?因为不同竞争程度需要不同的"锁力度":
• 偏向锁:只有一个人上厕所,门上挂个写着你名字的牌子就行,不用真锁(记录线程ID,零开销)
• 轻量级锁:两个人偶尔交替用,不用真锁门,谁进门前 CAS 看一眼牌子是否被改(自旋等待,省了操作系统调度)
• 重量级锁:一堆人抢一个厕所,CAS 自旋太浪费 CPU,直接上真锁(OS 互斥量),没抢到的人被操作系统挂起睡眠
JVM 自动根据竞争激烈程度"逐步升级",尽量用最轻量的方式——这就是 JDK 1.6+ 的核心优化。
面试中 synchronized 和 ReentrantLock 的对比是必考题。加分回答:能说出 synchronized 的锁升级(偏向→轻量→重量),以及 AQS 的 state + CLH 队列模型。
| 特性 | 是否保证 | 说明 |
|---|---|---|
| 可见性 | ✅ 保证 | 写操作强制刷回主内存,读操作从主内存加载。基于内存屏障(Memory Barrier)实现 |
| 有序性 | ✅ 保证 | 禁止指令重排序(happen-before 规则)。典型场景:DCL 单例中防止对象半初始化 |
| 原子性 | ❌ 不保证 | i++ 不是原子操作(读→改→写三步),volatile 无法保证。需要 AtomicInteger 或 synchronized |
volatile = 公告栏:所有人看同一块公告栏(主内存),有人改了立刻刷新上去所有人都能看到(可见性);公告的顺序不能乱贴(有序性);但两个人同时改同一个公告可能冲突覆盖(不保证原子性)。
CAS 是乐观锁的核心,也是所有原子类的底层实现。CAS = 抢红包:看到红包先记住当前金额(期望值),点的时候如果金额没变就抢到(更新成功),如果已经被别人抢了(值变了)就重试。
CAS 三大问题:
• ABA 问题:值从 A→B→A,CAS 认为没变。解决:AtomicStampedReference(加版本号)
• 自旋开销:竞争激烈时 CAS 一直失败重试,CPU 空转。解决:适当控制并发度
• 只能保证单变量原子:多个变量需要用锁或 AtomicReference 包装
| 类别 | 常用类 | 场景 |
|---|---|---|
| 基本类型 | AtomicInteger / AtomicLong / AtomicBoolean | 计数器、序列号生成 |
| 引用类型 | AtomicReference / AtomicStampedReference | 无锁数据结构、解决ABA |
| 数组类型 | AtomicIntegerArray / AtomicLongArray | 数组元素的原子更新 |
| 字段更新器 | AtomicIntegerFieldUpdater | 已有类的 volatile 字段原子更新 |
| 累加器(JDK8) | LongAdder / LongAccumulator | 高并发计数,比 AtomicLong 快(分段CAS) |
面试官问"volatile 和 synchronized 的区别",标准答案:volatile 保证可见性+有序性但不保证原子性,synchronized 三者都保证。然后主动举 DCL 单例的例子,基本稳了。
线程本地变量——每个线程独享一份副本,线程之间互不干扰。ThreadLocal = 每个线程有自己的保险箱,别人拿不到你的东西,你也不需要和别人共用。
关键:ThreadLocalMap 是 Thread 的成员变量,不是 ThreadLocal 的。每个线程有自己的 Map,所以天然隔离。
ThreadLocal vs synchronized 的区别:synchronized 是让多个线程排队访问同一份数据(共享+互斥);ThreadLocal 是让每个线程各拥有一份数据(隔离+无竞争)。前者用锁保证安全,后者用隔离消除竞争。典型场景:每个线程维护自己的 SimpleDateFormat、数据库连接、用户会话上下文。
解决方案:
• 使用完必须调用 threadLocal.remove()(在 finally 块中)
• 线程池场景尤其重要!线程被复用,ThreadLocalMap 一直存在
线程池 + ThreadLocal 是内存泄漏重灾区。面试中一定要主动说出:1) 为什么用 WeakReference 还会泄漏 2) 必须手动 remove()。这是区分"看过八股"和"真正理解"的关键点。
| 工具类 | 作用 | 典型场景 | 能否复用 |
|---|---|---|---|
| CountDownLatch | 让一个线程等待其他线程完成。计数器递减到 0 才放行 | 主线程等待 N 个子任务全部完成后再汇总结果(如多线程查询后合并) | ❌ 一次性的 |
| CyclicBarrier | 让一组线程互相等待,全部到齐后一起继续执行 | 多线程分阶段计算,每阶段所有线程都完成后才进入下一阶段(如并行数据处理) | ✅ 可循环复用 |
| Semaphore | 控制同时访问的线程数量(许可计数) | 限制并发访问资源数(如数据库连接池最多 20 个、接口限流) | ✅ acquire/release |
CountDownLatch vs CyclicBarrier 是高频对比题。核心区别:① CountDownLatch 是计数器递减,CyclicBarrier 是等齐后一起走 ② CountDownLatch 一次性,CyclicBarrier 可复用 ③ CountDownLatch 是一个线程等其他线程,CyclicBarrier 是线程之间互相等。
① 核心线程未满 → 创建核心线程执行任务
② 核心线程满了 → 任务进入工作队列
③ 队列也满了 → 创建临时线程(到 maximumPoolSize)
④ 全满 → 触发拒绝策略
关键点:先入队列,再扩线程,不是先创建到最大线程数再入队列。
默认不会。核心线程即使空闲也会一直存活。
设置 allowCoreThreadTimeOut(true) 后,核心线程在 keepAliveTime 时间内空闲也会被回收。
① AbortPolicy(默认):抛 RejectedExecutionException
② CallerRunsPolicy:由调用者线程自己执行
③ DiscardPolicy:默默丢弃,不抛异常
④ DiscardOldestPolicy:丢弃队列最老任务,重新提交当前任务
生产推荐 CallerRunsPolicy 或自定义策略(日志+持久化+重试)。默认的 AbortPolicy 在高并发下会导致服务异常。
• CPU 密集型:CPU核数 + 1。线程多了上下文切换反而降低性能。
• IO 密集型:CPU核数 × 2 或 CPU核数 / (1 - 阻塞比)。线程等IO时CPU空闲,可以多配。
• 混合型先按IO密集配,再压测调优。
• newFixedThreadPool / newSingleThreadExecutor:使用 无界 LinkedBlockingQueue,任务堆积会导致 OOM
• newCachedThreadPool / newScheduledThreadPool:maximumPoolSize = Integer.MAX_VALUE,可能创建海量线程导致 OOM
应使用 ThreadPoolExecutor 显式指定参数,确保队列有界、线程数可控。
• LinkedBlockingQueue:链表实现,默认无界(必须指定容量!),吞吐量较高
• ArrayBlockingQueue:数组实现,有界,创建时必须指定容量,公平/非公平可选
• SynchronousQueue:不存储元素,每个put必须等take,适合直接交接(CachedThreadPool用)
• PriorityBlockingQueue:支持优先级排序的无界队列
• DelayQueue:延迟队列,元素到期才能取出
生产推荐 ArrayBlockingQueue(有界+公平)或指定容量的 LinkedBlockingQueue。
• execute():提交 Runnable,无返回值,异常直接抛出
• submit():提交 Runnable/Callable,返回 Future,异常封装在 Future.get() 中
注意:submit() 的任务如果抛异常,不会直接打印,必须 future.get() 时才会看到。这是常见坑!
• shutdown():平滑关闭。不再接受新任务,等待已提交的任务执行完
• shutdownNow():立即关闭。尝试中断正在执行的任务,返回未执行的任务列表
• awaitTermination():配合shutdown使用,阻塞等待所有任务完成或超时
5 种状态(存在 ctl 的高 3 位):
• RUNNING(111):接受新任务,处理队列任务
• SHUTDOWN(000):不接受新任务,但处理队列任务(shutdown()后)
• STOP(001):不接受新任务,不处理队列,中断进行中任务(shutdownNow()后)
• TIDYING(010):所有任务终止,workerCount=0
• TERMINATED(011):terminated()方法执行完
转换:RUNNING → SHUTDOWN → STOP → TIDYING → TERMINATED
ThreadPoolExecutor 提供了运行时修改参数的方法:
• setCorePoolSize():动态修改核心线程数
• setMaximumPoolSize():动态修改最大线程数
• setKeepAliveTime():动态修改空闲时间
美团的做法:配置中心(如 Apollo/Nacos)监听参数变更 → 调用上述方法动态调整 → 结合监控告警(队列长度、活跃线程数、拒绝次数)实现自适应。
• execute() 提交的任务,异常会直接抛出,线程终止,线程池创建新线程替代
• submit() 提交的任务,异常被吞到 Future 中,必须 future.get() 才能捕获
• 3 种处理方式:
1. try-catch 包住 run() 方法体
2. 实现 afterExecute() 钩子(继承 ThreadPoolExecutor)
3. 设置 UncaughtExceptionHandler
• ThreadPoolExecutor 内部用 HashSet<Worker> 管理所有线程
• Worker 继承 AQS,实现了 不可重入锁(避免任务中调用池方法导致死锁)
• Worker.runWorker() 是核心方法:不断从队列 getTask() 取任务执行
• 核心线程取任务用 queue.take()(阻塞等待),临时线程用 queue.poll(keepAliveTime)(超时回收)
这就是为什么核心线程不会死、临时线程会超时回收的底层原因。
• 实现:synchronized 是 JVM 层面(字节码),ReentrantLock 是 API 层面
• 释放:synchronized 自动释放,ReentrantLock 必须在 finally 中手动 unlock()
• 可中断:synchronized 不可中断,ReentrantLock 支持 lockInterruptibly()
• 公平性:synchronized 非公平,ReentrantLock 可选公平/非公平
• 条件:synchronized 一个 wait/notify,ReentrantLock 多个 Condition
• 尝试:synchronized 阻塞等待,ReentrantLock tryLock() 非阻塞
AQS = volatile int state + CLH 双向队列
• state:表示锁状态。独占锁 0/1,共享锁可大于 1
• CLH 队列:存放等锁的线程,FIFO 排队
• 抢锁:CAS 修改 state,成功则获取锁
• 排队:抢不到就加入 CLH 队列尾,LockSupport.park() 挂起
• 释放:state 归零,唤醒后继节点 unpark()
模板方法模式:AQS 搞定排队/阻塞逻辑,子类只需实现 tryAcquire/tryRelease。
不能完全保证。volatile 只保证 可见性 + 有序性,不保证原子性。
• 可见性:一个线程修改,其他线程立即可见(内存屏障)
• 有序性:禁止指令重排序(DCL 单例的关键)
• 原子性:i++ 不是原子操作,volatile 无法保证。需要 AtomicInteger 或 synchronized
CAS = Compare And Swap,乐观锁核心实现。比较当前值与期望值,相同则更新,不同则重试。
三大问题:
① ABA 问题:A→B→A 看起来没变。解决:AtomicStampedReference(版本号)
② 自旋开销:竞争激烈时 CAS 反复失败,CPU 空转
③ 只能保证单变量:多变量需 AtomicReference 或加锁
ThreadLocalMap 的 key 是 WeakReference,GC 时 key 被回收变为 null,但 value 是强引用,仍被 Entry 引用 → 无法回收 → 泄漏。
线程池场景更严重:线程被复用,ThreadLocalMap 一直存在。
解决方案:使用完必须 threadLocal.remove(),放在 finally 块中。
• CountDownLatch:一个线程等 N 个线程完成(计数器递减),一次性,不可复用
• CyclicBarrier:N 个线程互相等齐后一起执行,可循环复用(自动 reset)
• Semaphore:控制并发线程数(许可计数),acquire/release
记忆:CountDown = 倒计时(减法),Cyclic = 循环栅栏(等齐),Semaphore = 信号灯(限流)
JDK 1.6+ 优化,锁从低到高升级:
无锁 → 偏向锁 → 轻量级锁 → 重量级锁
• 偏向锁:只有一个线程访问,直接在对象头记录线程 ID,无需 CAS
• 轻量级锁:两个线程交替访问,CAS 自旋抢锁
• 重量级锁:竞争激烈,升级为 OS 互斥量,线程阻塞
• 锁只能升级,不能降级(GC 时可以)
6 种状态:NEW → RUNNABLE → BLOCKED / WAITING / TIMED_WAITING → TERMINATED
• BLOCKED:等待 synchronized 锁(被动等待)
• WAITING:调用 wait()/join()/park()(主动等待,需要被其他线程唤醒)
• TIMED_WAITING:调用 sleep(n)/wait(n)/parkNanos(n)(超时自动唤醒)
7大参数速记:
核心数 → 最大数 → 存活时间 → 时间单位 → 工作队列 → 线程工厂 → 拒绝策略
执行流程速记:
核心线程 → 入队列 → 临时线程 → 拒绝策略
拒绝策略速记:
Abort抛异常 / CallerRuns自己跑 / Discard丢掉 / DiscardOldest丢老的
线程数速记:
CPU密集 = 核数+1 | IO密集 = 核数×2 或 核数/(1-阻塞比)
锁对比速记:
synchronized=JVM自动/非公平 | ReentrantLock=API手动/可公平/可中断
AQS 速记:
volatile state + CLH队列 + CAS抢锁 + park挂起 + unpark唤醒
volatile 速记:
可见性✅ 有序性✅ 原子性❌ → i++用AtomicInteger
ThreadLocal 速记:
线程隔离 ✅ | 内存泄漏 ⚠️ | 用完必须remove()
面试不是一个个孤立问题,而是一条链。以下是最常见的追问路径:
链路 1:线程池 → 锁 → AQS
"说说线程池的执行流程" → "线程池内部 Worker 用了什么锁?" → "AQS 原理是什么?" → "synchronized 和 ReentrantLock 区别?"
链路 2:volatile → CAS → 原子类
"volatile 保证什么?" → "i++ 用 volatile 行吗?" → "那用什么?CAS 原理?" → "ABA 问题怎么解决?"
链路 3:ThreadLocal → 内存泄漏 → 线程池
"ThreadLocal 原理?" → "为什么用 WeakReference?" → "还会内存泄漏吗?" → "线程池中怎么用 ThreadLocal?"
链路 4:同步工具 → 线程通信
"CountDownLatch 和 CyclicBarrier 区别?" → "Semaphore 怎么用?" → "wait/notify 和 Condition 区别?"
并发面试三原则:① synchronized 锁对象要明确,静态方法锁 Class、实例方法锁 this → ② 线程池参数要压测,核心线程数不是拍脑袋 → ③ volatile 保证可见性不保证原子性,能用 Atomic 就别用 synchronized