Java 并发 —— 从 JMM/工具类到锁机制与无锁设计
10 题 · 平均 3.1/5 · low 3.33 · medium 3 · high 3 · 第 1 轮
**核心考点定位**:能否说清 HB 是"可见性与有序性的契约",并背出核心规则。
**知识点拆解**:
1. **本质**:HB 是 JMM 定义在操作之间的**偏序关系**。若 A hb B,则 A 的所有写对 B 可见——它回答的是"一个线程的写何时对另一个线程可见"。
2. **核心规则**:
- 程序顺序:同一线程内,前面的操作 hb 后面的操作
- 监视器锁:对锁的解锁 hb 后续对同一把锁的加锁
- volatile:对 volatile 变量的写 hb 后续对它的读
- 线程启动:`start()` hb 该线程内的所有操作
- 线程终止:线程内所有操作 hb 其他线程从 `join()` 返回
- 传递性:A hb B,B hb C ⇒ A hb C
3. **实现**:JVM 用内存屏障(LoadLoad/StoreStore/LoadStore/StoreLoad)落地 HB;volatile 插屏障、synchronized 靠 Monitor。
> 按「底层原理 → 设计权衡 → 工程实践」递进排列。
**考察方向**:
- **深度**:HB 的偏序性、与 as-if-serial 的关系
- **广度**:与内存屏障、volatile、synchronized 的关系
- **应用**:DCL 单例为什么必须给实例加 volatile
**延伸递进面试题**:
1. **volatile 的写/读建立什么 HB?**
- 解答要点:写 hb 后续读;保证可见性 + 有序性,不保证原子性
2. **为什么 DCL 单例要加 volatile?**
- 解答要点:HB 防止"引用赋值先于构造完成"被其他线程看到半构造对象
3. **happens-before 和 as-if-serial 的区别?**
- 解答要点:as-if-serial 是单线程内的重排约束;HB 是跨线程可见性契约
**【一句话结论】**
面试官您好,happens-before 是 JMM 规定的一种偏序关系,用来保证"前面操作的结果对后面操作可见"。
**【三点展开】**
第一,它是**语义契约不是实现**——描述的是可见性与有序性,不是"禁止重排"本身;
第二,核心规则有程序顺序、监视器锁、volatile 写读、线程 start/join、以及传递性;
第三,底层靠**内存屏障**落地,volatile 插屏障、synchronized 靠 Monitor 的解锁-加锁规则。
**【一句边界】**
当然,它只管可见性和有序性,**不保证原子性**——i++ 依然线程不安全,该上锁还得上锁。
**核心考点定位**:三者的语义差异 + 底层实现方式。
**知识点拆解**:
1. **CountDownLatch**:一次性的"等待计数归零"。AQS 共享模式,state = 计数;`await()` 阻塞、`countDown()` 减到 0 唤醒全部等待线程。
2. **CyclicBarrier**:可循环的栅栏,N 个线程互相等待到齐再一起放行。**ReentrantLock + Condition 实现**(不是 AQS),可传 barrierAction、可 `reset()` 复用。
3. **Semaphore**:许可信号量,`acquire/release` 控制同时通过的线程数。AQS 共享模式,state = 剩余许可。
4. **一句话区分**:CDL 等"事件"、一次性;CB 等"彼此"、可复用;Semaphore 控"数量"。
**考察方向**:
- **深度**:AQS 共享模式的传播唤醒(`tryAcquireShared` + `doReleaseShared`)
- **广度**:与 Phaser 对比;CB reset 的坑
- **应用**:CDL 做并行汇总、CB 做分阶段并行、Semaphore 做限流/连接池
**延伸递进面试题**:
1. **CyclicBarrier 为什么能复用,CDL 为什么不能?**
- 解答要点:CB 靠 ReentrantLock+Condition 可重置代(generation);CDL 的 state 归零即终结
2. **Semaphore 能替代限流器吗?和令牌桶区别?**
- 解答要点:Semaphore 控"并发数",令牌桶控"速率";瞬时突发语义不同
3. **AQS 共享模式如何唤醒多个线程?**
- 解答要点:`setHeadAndPropagate` 向后传播,连续唤醒共享节点
**【一句话结论】**
面试官您好,三者都是并发协调工具,但语义和实现都不同。
**【三点展开】**
第一,**CountDownLatch** 是一次性的"等待事件",主线程 `await` 等计数归零,底层是 AQS 共享模式;
第二,**CyclicBarrier** 是可循环的"互相等待到齐",底层是 `ReentrantLock + Condition`——注意它**不走 AQS**;
第三,**Semaphore** 是许可信号量,控制同时通过的线程数,底层是 AQS 共享模式,常用来限流。
**【一句边界】**
当然,CyclicBarrier 的 `reset()` 会打断已等待的线程抛 `BrokenBarrierException`,生产环境要慎用 reset。
**核心考点定位**:考察对 ThreadPoolExecutor 的参数体系、执行流程、工程选型的全链路掌握。
**知识点拆解**:
1. **池化思想**:线程创建成本(内核态切换、栈内存、GC 压力);池化收益 = 复用 + 限流 + 隔离
2. **7 大核心参数**:corePoolSize / maximumPoolSize / keepAliveTime / unit / workQueue / threadFactory / handler
3. **执行流程**(三步判断):core → 队列未满 → max → 拒绝策略;队列优先级高于 max 扩容
4. **4 种拒绝策略**:AbortPolicy(默认抛异常)、DiscardPolicy(静默丢弃)、DiscardOldestPolicy(丢队列头)、CallerRunsPolicy(调用者执行,天然限流)
5. **4 种队列**:ArrayBlockingQueue(有界)、LinkedBlockingQueue(可无界 OOM 风险)、SynchronousQueue(不存储)、PriorityBlockingQueue(优先级)
6. **5 态**:RUNNING → SHUTDOWN → STOP → TIDYING → TERMINATED
7. **Executors 禁用**:FixedThreadPool/SingleThreadExecutor 用无界队列 OOM;CachedThreadPool 无 max 线程暴涨;ScheduledThreadPool 无界队列
8. **参数估算**:CPU 密集 N_cpu×1;IO 密集 N_cpu×2 或 N_cpu×(1+wait/compute)
9. **监控**:JMX MBean 暴露 queueSize/activeCount/completedTaskCount/rejectedCount
**考察方向**:
- 深度:execute() 源码执行路径,为什么队列优先级高于 max?
- 广度:与 ForkJoinPool / 虚拟线程对比
- 应用:线程暴涨排查、队列积压排查、动态调优
**延伸递进面试题**:
1. **ThreadPoolExecutor.execute 的源码流程?**
- 解答要点:三关判断(core → queue → max),CAS 修改 workerCount,addWorker 创建线程
2. **为什么队列满了才扩非核心线程?**
- 解答要点:避免频繁创建销毁线程,队列作为缓冲池;代价是延迟升高
3. **线程暴涨如何排查?**
- 解答要点:jstack 抓栈 → 看线程名 → 定位提交方 → 看 queue 是否无界
4. **ForkJoinPool 和 ThreadPoolExecutor 区别?**
- 解答要点:ForkJoin 用工作窃取,无界任务队列,适合并行任务;TP 适合独立任务
5. **JDK 21 虚拟线程能替代线程池吗?**
- 解答要点:虚拟线程 M:N 调度,阻塞时释放载体线程;CPU 密集仍需池;不适合 synchronized 大量竞争
6. **动态线程池如何落地?**
- 解答要点:阿里 Hippo4j / 美团动态线程池;配置中心推送 + JMX 回调 + 告警
面试官您好,线程池本质是池化思想,核心目标是复用线程 + 限流 + 隔离。
参数方面有 7 个:corePoolSize 常驻线程数、maximumPoolSize 最大线程数、keepAliveTime 非核心线程空闲回收时间、unit 时间单位、workQueue 阻塞队列、threadFactory 线程工厂(一定要给线程命名,方便排查)、handler 拒绝策略。
执行流程是三步判断:任务进来先看当前线程数是否小于 core,是就建核心线程;否则看队列是否满,不满入队;否则看是否小于 max,是就建非核心线程;都不行触发拒绝策略。注意队列优先级高于 max 扩容,这是很多人讲错的地方。
拒绝策略 4 种内置:AbortPolicy 抛异常(默认)、DiscardPolicy 静默丢弃、DiscardOldestPolicy 丢队列头重试、CallerRunsPolicy 调用者自己执行——这个我比较常用,天然限流让上游感受慢。
队列选择我一般看场景:CPU 密集用 SynchronousQueue 或无队列,max 拉高;IO 密集用 ArrayBlockingQueue 有界队列控内存;任务有优先级用 PriorityBlockingQueue。
我们项目里禁用 Executors,因为它内部用无界队列或无上限线程,容易 OOM。参数估算用 N_cpu × 2(IO 密集)做初值,再压测调优。动态调优接了配置中心,运行时改 corePoolSize / maximumPoolSize,配 JMX 监控 activeCount、queueSize、rejectedCount 告警。
我们踩过的坑是线程暴涨:某个下游变慢,队列积压,线程飙到几千,最后 JVM OOM。定位靠 threadFactory 命名的线程名 + jstack 抓栈。之后改成有界队列 + CallerRunsPolicy,队列满后直接压回上游,问题就解决了。
---
**核心考点定位**:能不能列出 4 种方式,并讲清继承 vs 接口、有无返回值的取舍。
**知识点拆解**:
1. **继承 Thread**:重写 `run()`,`new` 后 `start()`。受单继承限制,不推荐。
2. **实现 Runnable**:把任务与线程解耦,可配合线程池、lambda,推荐。
3. **实现 Callable**:`call()` 有返回值、可抛异常,需 `FutureTask` 包装(也实现了 Runnable)才能交给 Thread。
4. **线程池**:`ExecutorService.submit/execute`,复用线程、可控并发,**工程首选**。
**考察方向**:
- **深度**:`Thread.start()` → native `start0()` → JVM 创建内核线程
- **广度**:Future/FutureTask、CompletableFuture
- **应用**:为什么大厂规范强制用线程池
**延伸递进面试题**:
1. **Runnable 和 Callable 区别?**
- 解答要点:Callable 有返回值+可抛异常,需 FutureTask 适配
2. **调用 run() 和 start() 有什么区别?**
- 解答要点:run() 是普通方法调用(当前线程执行);start() 才创建新线程
3. **为什么阿里规范不推荐 new Thread / Executors?**
- 解答要点:无法管控资源、无界队列 OOM、线程暴涨
**【一句话结论】**
面试官您好,创建线程标准有 4 种方式,实际开发首选线程池。
**【三点展开】**
第一,**继承 Thread** 重写 run,简单但受单继承限制;
第二,**实现 Runnable / Callable**——Callable 有返回值、能抛异常,要用 FutureTask 包装;
第三,**线程池** ExecutorService 提交任务,线程复用、并发可控,是工程首选。
**【一句边界】**
当然,简单的一次性任务用 Runnable 就行,但生产环境不要裸 `new Thread`,也不要用 `Executors` 快捷工厂。
**核心考点定位**:考察对 `Thread` 类 API 的精确掌握,尤其"存活"与"运行"两个概念的边界。
**知识点拆解**:
1. **底层原理**:`isAlive()` 的判断逻辑是 `state != NEW && state != TERMINATED`,本质是排除两端——没启动的、已结束的不算活
2. **设计权衡**:JDK 同时提供 `isAlive()` 和 `isTerminated()` 两个方法,覆盖正反向两种判断习惯;`!isTerminated()` 是官方推荐的等价写法
3. **工程实践**:主线程 join 子线程前先检查 `isAlive()` 避免 NPE;线程池复用前也检查(不过线程池有自己的一套,不用这个方法)
**考察方向**:
- **深度**:`isAlive()` 的 native 层实现(`_thread` 字段 + 状态机)
- **广度**:与 `getState()`、`isTerminated()`、`join()` 的组合用法
- **应用**:线程管理器中判断"是否需要补充新线程"
**延伸递进面试题**:
1. **NEW 状态算存活吗?**
- 解答要点:不算。`new Thread()` 没调 start,`isAlive()` 返回 false
2. **TERMINATED 和 NEW 怎么区分?**
- 解答要点:用 `getState()` 精确判断;`isAlive()` 只告诉你"是否活着",不区分是"没出生"还是"已死亡"
3. **isAlive() 是线程安全的吗?**
- 解答要点:是。它内部读的是 volatile 状态字段,无锁读取
面试官您好,Java 判断线程存活最直接的 API 是 `thread.isAlive()`,它内部判断 `state != NEW && state != TERMINATED`——只要不是"还没启动"或"已经结束",就算存活。反向判断可以用 `!thread.isTerminated()`,语义上更明确。
如果要用更底层的方式判断,可以先调 `thread.getState()` 拿到 `Thread.State` 枚举值,然后自己排除 NEW 和 TERMINATED 两个值。这种方式在需要区分"没启动"和"已死亡"的场景下更灵活。
有三个容易搞错的点我要强调:第一,NEW 不算存活——线程 new 出来但没调 start 的话,`isAlive()` 返回 false,很多人以为"存在对象就活着"是错的;第二,BLOCKED / WAITING / TIMED_WAITING 都算存活,它们只是"没在跑",不是"死了";第三,`isAlive()` 是线程安全的,底层读的是 volatile 状态,可以放心并发调用。
工程上常见用法:主线程在 join 子线程前先检查 `isAlive()`,避免对一个从未 start 的线程调 join 抛异常;线程管理框架在补充新线程前也会检查旧线程是否还在。
---
**核心考点定位**:考察对 `synchronized` 底层优化机制的理解,尤其偏向锁的生命周期与 JDK 版本演进。
**知识点拆解**:
1. **底层原理**:锁升级本质是 JIT 编译器针对不同竞争程度选择不同实现策略——**单线程走 CAS 无竞争、双线程走自旋、多冲突走内核阻塞**,全部靠 Mark Word 里的 4 位锁标志位切换
2. **设计权衡**:偏向锁在单线程场景零开销,但撤销代价高(需要等所有持有偏向锁的代码块执行完或全局安全点);轻量级锁自旋在 CPU 空闲时好,CPU 忙时会浪费;重量级锁有线程挂起的系统调用开销,但真正多冲突时最省心
3. **工程实践**:现代 JDK(16+)默认关闭偏向锁,因此实际开发中"锁升级"更多是"轻量级 → 重量级"两态;业务代码应尽量避免长时间持有锁,让锁停留在轻量级
**考察方向**:
- **深度**:Mark Word 4 位标志位变化、Lock Record 结构、偏向锁撤销的 safepoint 机制
- **广度**:与 `ReentrantLock` 的公平/非公平、`StampedLock` 的读优化对比
- **应用**:如何用 `jcmd`、`-XX:+PrintLocks` 观察锁状态;如何避免锁升级到重量级
**延伸递进面试题**:
1. **偏向锁为什么在 JDK 15 被默认关闭?**
- 解答要点:偏向撤销代价高,G1 等现代 GC 的 safepoint 频繁,撤销的延迟代价超过了本身收益
2. **如何判断锁是否升级到重量级?**
- 解答要点:`jcmd <pid> GC.heap_dump` 后用 MAT 查看 Mark Word;或开启 `-XX:+PrintLocks`
3. **synchronized 和 ReentrantLock 的选型依据是什么?**
- 解答要点:默认用 synchronized(简单、有 JIT 优化);需要超时、中断、公平、多 condition 时选 ReentrantLock
面试官您好,`synchronized` 的锁升级从 JDK 1.5 就引入了,本质是 JIT 根据竞争程度选择不同实现策略,全部靠对象头 Mark Word 里的 4 位标志位切换。
一共四态。**无锁**是初始状态,Mark Word 存对象元数据。**偏向锁**在单线程访问时启用,把当前线程 ID 写进 Mark Word,后续这个线程再访问时 CAS 校验线程 ID 匹配就无需加解锁,开销接近零。JDK 15 之后偏向锁**默认关闭**,JDK 18 **彻底移除**,原因是撤销代价高、G1 的 safepoint 频繁。**轻量级锁**在有第二个线程竞争时启用,把 Mark Word 拷贝到线程栈上的 Lock Record,然后 CAS 自旋,其他线程不会挂起。**重量级锁**在自旋超过阈值或多线程争抢时启用,通过操作系统 mutex 挂起竞争线程,有系统调用开销。
升级方向是**不可逆**的,但每次解锁后 Mark Word 会回到无锁状态,下次加锁可能重新走偏向锁。JIT 还有两个额外优化:**锁消除**(局部变量 StringBuilder 的 synchronized 会被直接删掉)、**锁粗化**(连续对同一对象加解锁合并成大锁)。
工程上,JDK 8 是"四态全开"的黄金版本;JDK 16+ 实际就是"轻量级 → 重量级"两态。业务要避免长时间持有锁,让锁停留在轻量级阶段。选型上默认用 synchronized,需要超时、中断、公平或多 condition 时才用 ReentrantLock。
---
**核心考点定位**:无锁的手段清单 + 各自边界 + ABA 治理。
**知识点拆解**:
1. **CAS**:`Unsafe.compareAndSwapInt`,靠 CPU 的 `cmpxchg` + `lock` 前缀保证原子;三要素 expected/update/new,失败重试(自旋)。
2. **原子类**:`AtomicInteger/AtomicReference/AtomicLong`;高并发计数用 `LongAdder`(分段 Cell 降低热点)优于 `AtomicLong`。
3. **无共享状态**:不可变对象(final + 构造期完成)、ThreadLocal、栈封闭。
4. **ABA 治理**:`AtomicStampedReference`(版本号)、`AtomicMarkableReference`(布尔标记)。
5. **边界**:只能保证**单个变量**的原子性;自旋在竞争激烈时浪费 CPU;复合操作要 `AtomicReference` 或直接加锁。
**考察方向**:
- **深度**:CAS 在 CPU 层怎么保证原子(缓存锁定/MESI)
- **广度**:与 volatile 的关系(可见性靠 volatile)、与 cell 分段
- **应用**:并发计数选型、无锁队列(Disruptor)
**延伸递进面试题**:
1. **CAS 的 ABA 怎么解决?**
- 解答要点:版本号/时间戳 `AtomicStampedReference`
2. **LongAdder 为什么比 AtomicLong 快?**
- 解答要点:分段 Cell,分散热点;`sum()` 是弱一致
3. **CAS 一定自旋吗?竞争大怎么办?**
- 解答要点:不一定;高竞争退化为自旋浪费,改用 LongAdder 或加锁
**【一句话结论】**
面试官您好,无锁化编程本质是"用乐观重试 + 原子指令替代阻塞锁"。
**【三点展开】**
第一,核心手段是 **CAS 和原子类**,CAS 靠 CPU 的 `cmpxchg` 加锁前缀保证原子,失败就自旋重试;
第二,它解决的是**单变量**的原子更新,复合操作要用 `AtomicReference` 打包,高并发计数用 `LongAdder` 分段更好;
第三,还有**无共享状态**的思路——不可变对象、ThreadLocal,从根上避开竞争。
**【一句边界】**
当然,CAS 有 ABA 问题要用版本号,而且竞争激烈时自旋反而浪费 CPU——这时不如退回到锁或分段。
**核心考点定位**:能否说清"原子性 = 互斥"、"可见性 = 解锁刷内存"、"有序性 = 禁止指令重排"这三条底层机制,以及 happens-before 规则。
**知识点拆解**:
1. **原子性 ← Monitor 互斥机制**:
- 每个对象有一个隐含的 **Monitor(管程)**,包含 owner 指针
- 线程进入 synchronized 块时通过 CAS 尝试设置 Monitor 的 owner;成功后独占执行;其他线程进不来
- 重量级锁状态下线程通过 OS 阻塞挂起到 entryList;轻量级锁通过自旋 CAS 竞争
2. **可见性 ← 解锁刷内存 + 内存屏障**:
- **解锁时**:把工作内存中的修改**强制刷回主存**(StoreLoad 屏障)
- **加锁时**:**强制从主存 reload** 到工作内存,丢弃本地缓存
- 这就保证了"解锁 → 加锁"之间的修改对所有线程可见
3. **有序性 ← 禁止指令重排**:
- JIT 编译器被禁止把 synchronized 块**内部**的指令重排到块**外部**
- CPU 层面的 StoreLoad 屏障防止跨块重排
- 注意:这是**指令级**的有序性,不是"线程获取锁的顺序"
4. **happens-before 规则**(JMM 第 17 条):
- 对一个 synchronized 块的解锁,happens-before 后续对**同一把锁**的加锁
- 这条规则是"可见性 + 有序性"能被 JIT 优化的契约基础
5. **三大特性对比 volatile**:
- volatile:可见性 ✓、有序性 ✓、原子性 ✗(i++ 三步不能打包)
- synchronized:可见性 ✓、有序性 ✓、原子性 ✓(互斥机制保证复合操作整体执行)
- 所以 synchronized 是 volatile 的"超集",但开销更大
> 按「底层原理 → 设计权衡 → 工程实践」递进排列。
**考察方向**:
- **深度**:能否说清 Monitor + 内存屏障 + JIT 重排禁止这条链路
- **广度**:能否区分 synchronized 和 volatile 各自覆盖的特性
- **应用**:能否举出代码里为什么 synchronized 块比 volatile 更强的场景
**延伸递进面试题**:
1. **synchronized 能保证原子性,为什么不用 volatile 也能保证可见性和有序性?**
- 解答要点:volatile 加内存屏障保证可见+有序,但读-改-写三步仍会交错;synchronized 用互斥把三步包成一个原子块
2. **synchronized 是公平锁吗?为什么说是非公平锁?**
- 解答要点:非公平锁。JVM 内部不保证先到先得;多个线程竞争时随机调度。需要公平性时用 ReentrantLock(true)
3. **happens-before 有哪些传递性规则?**
- 解答要点:锁规则(释放→获取)、volatile 规则(写→读)、传递性、启动线程、传递性;是 JMM 保证并发正确性的最小契约
面试官您好,synchronized 同时保证三个特性:**原子性、可见性、有序性**,但每一条的底层机制是不同的,我分开说。
**先说原子性**。原子性的本质是"互斥"——同一时刻只能有一个线程在 synchronized 块里执行。它的底层是 **Monitor(管程)机制**。每个对象都隐含一个 Monitor 对象,里面有个 owner 指针记录当前的持锁线程。线程进入 synchronized 块时,先尝试用 CAS 把 owner 设成自己;成功了就独占执行,失败了就在队列里等。重量级锁状态下其他线程会挂起到 entryList 阻塞等待,轻量级锁下通过自旋 CAS 竞争。所以从"互斥"这个角度看,原子性其实是锁的基本性质——你进得去,别人就进不来,操作就整体执行。
**再说可见性**。可见性靠的是**解锁和加锁时强制刷内存**。具体规则是:
- **解锁时**:把工作内存里的修改**强制刷回主存**,并加 StoreLoad 内存屏障,确保之前的所有写操作都被刷出去
- **加锁时**:**丢弃本地工作内存**,强制从主存 reload 最新值
这样"解锁 → 加锁"之间的修改对其他线程一定可见。JMM 把它总结成一条规则:**对一个 synchronized 块的解锁,happens-before 后续对同一把锁的加锁**。这是 JMM 第 17 条规则,是并发编程可见性的契约基础。
**第三个是有序性**。这里的有序性不是指"线程获取锁的顺序",而是**指令重排**——JIT 编译器不会把 synchronized 块内部的指令重排到块外部去。CPU 层面的 StoreLoad 屏障也保证了跨块的指令顺序不会被破坏。这个很重要,比如经典的 DCL 单例如果不加 volatile,`new` 指令会被重排成"分配内存→赋引用→初始化构造器",不加锁的话其他线程可能看到半构造实例。synchronized 块里因为有序性保证,指令不会被这样重排。
**对比一下 volatile**:volatile 只保证可见性和有序性,但**不保证原子性**——i++ 是读-改-写三步,volatile 只能让每步都可见,但三步之间可能交错。synchronized 通过互斥机制把三步包成一个整体,所以三个特性都保证。所以可以说 synchronized 是 volatile 的"超集",但开销也更大。
**关于公平性**——这里要纠正一个常见误解:**synchronized 不是公平锁,它是非公平锁**。JVM 内部不保证先到先得,多个线程竞争时随机调度。如果需要公平性,必须用 `ReentrantLock(true)`。
这就是我对 synchronized 三大特性的理解。
---
**核心考点定位**:能否用数据定位锁竞争/上下文切换,并给出分层优化手段。
**知识点拆解**:
1. **定位**:
- `jstack` 看线程状态分布(大量 BLOCKED 指向 monitor 竞争)
- `pidstat -w` 看每秒上下文切换(cswch 自愿 / nvcswch 非自愿)
- `vmstat 1` 的 cs 列、JFR/火焰图的 "Java Monitor Blocked"、`-XX:+PrintLocks`
2. **归因**:并发线程数远超 CPU 核数(调度切换);临界区太长(持锁时间久);锁粒度过粗。
3. **优化**:
- **减小锁范围**:只锁必要代码,去掉临界区里的 IO/计算
- **降低锁粒度**:锁分段(ConcurrentHashMap 思路)、每对象独立锁
- **换锁类型**:读多写少用读写锁/StampedLock;高竞争计数用 LongAdder
- **去锁**:CAS 乐观更新、不可变、ThreadLocal、异步/缓存
- **削峰**:有界队列 + 幂等 + 限流,避免线程暴涨
**考察方向**:
- **深度**:上下文切换的成本构成(寄存器/栈/缓存失效)
- **广度**:与线程池调优、无锁、读写锁的联动
- **应用**:线上 CPU 飙高的完整排查链路
**延伸递进面试题**:
1. **怎么确认瓶颈是锁竞争而不是 CPU 计算?**
- 解答要点:BLOCKED 比例高 + cswch 高;若 RUNNABLE 高且 CPU 打满则偏计算
2. **锁分段怎么实现?**
- 解答要点:把一个锁拆成 N 个按 key 取模,降低竞争(ConcurrentHashMap 1.7 思路)
3. **自旋锁在什么场景反而更差?**
- 解答要点:竞争激烈时自旋空耗 CPU,不如直接阻塞挂起
**【一句话结论】**
面试官您好,我会先用数据确认瓶颈在锁竞争,再做分层优化。
**【三点展开】**
第一,**定位**:`jstack` 看大量 BLOCKED、`pidstat -w` 看上下文切换次数、火焰图看 monitor blocked,确认热点锁;
第二,**归因**:要么并发线程数远超 CPU 核数,要么临界区太长、锁粒度太粗;
第三,**优化**:缩小临界区、锁分段降粒度、读多写少换读写锁/StampedLock、高竞争计数用 LongAdder,必要时用 CAS/异步彻底去锁。
**【一句边界】**
当然,上下文切换不一定是坏事——要确认它真的没带来收益;也不能盲目"去锁",语义正确性优先于性能。
**核心考点定位**:能否讲清 RingBuffer 的无锁协调机制(序号)与低延迟手段(防伪共享、无 GC)。
**知识点拆解**:
1. **环形数组 + 预分配**:容量为 2 的幂,取模用位运算 `index & (size-1)`;元素在启动时全部预创建,运行期不再 new,避免 GC 抖动。
2. **序号(Sequence)协调**:
- 生产者声明位置:用 CAS 竞争 `cursor`/`next` 序号,多生产者用 `getAndIncrement` 抢占槽位
- 消费者:通过 `SequenceBarrier` 等待"可消费序号",用 `min(生产者序号, 依赖序号)` 决定能消费到哪
- 避免覆盖:生产者发布前检查"序号 - bufferSize"是否已被消费
3. **消除伪共享**:对序号字段做**缓存行填充**(padding 到 64 字节),避免相邻字段互相失效缓存行。
4. **批处理**:一次拿到连续可用区间,减少 CAS 和等待次数,提升吞吐。
**考察方向**:
- **深度**:多生产者如何用 CAS 保证序号不冲突;SequenceBarrier 的依赖等待
- **广度**:与 `ArrayBlockingQueue`(锁)、`ConcurrentLinkedQueue`(无界 CAS 链表)对比
- **应用**:日志/交易撮合等低延迟场景为何选 Disruptor
**延伸递进面试题**:
1. **Disruptor 为什么比 BlockingQueue 快?**
- 解答要点:无锁序号 + 预分配无 GC + 缓存行填充防伪共享 + 批处理
2. **多生产者如何避免序号冲突?**
- 解答要点:对 cursor 用 CAS 抢占,失败重试;或用 `Sequencer`
3. **为什么环形容量要取 2 的幂?**
- 解答要点:`index & (n-1)` 位运算代替取模,更快
**【一句话结论】**
面试官您好,RingBuffer 的核心是"环形数组 + 序号协调 + 无锁",本质是用序号代替锁来协调生产消费。
**【三点展开】**
第一,**空间预分配**:容量取 2 的幂,启动时把元素全部创建好,取模用位运算,运行期零 GC;
第二,**序号协调是无锁的关键**:生产者用 CAS 抢占写序号,消费者用 `SequenceBarrier` 按依赖等待可读序号,靠"序号差 ≥ 容量"判断能否覆盖;
第三,**低延迟的细节**:序号字段做缓存行填充消除伪共享,再靠批处理一次取连续区间,减少 CAS 和等待。
**【一句边界】**
当然,Disruptor 适合吞吐极高、依赖简单的场景;如果只是普通异步解耦,`BlockingQueue` 更简单够用。