← 学习门户 每日面试题 学习看板

2026-10-17

Java 并发 —— 从 JMM/工具类到锁机制与无锁设计

10 题 · 平均 3.1/5 · low 3.33 · medium 3 · high 3 · 第 1 轮

#1 什么是happens-before原则? low java-concurrency

题目

什么是happens-before原则?

我的回答

heppends-before原则是JMM,或者说JIT中防止指令重排的设定原则,规定了什么情况下不能进行重排,什么内容要在什么内容之前,在内存中指令要如何排列; 对应的底层依赖是读取和写入的规则,以及内存屏障等指令做支持; 具体的规则比较多,只说几个内存屏障相关的,读读屏障,读写屏障,写写屏障,写读屏障

点评

优点:抓住"规定先后顺序、防止乱序"的方向,也提到底层靠内存屏障。
不足:把 happens-before 等同于"防重排+内存屏障"是概念错位——HB 是 JMM 的**语义偏序契约**,用来定义"可见性+有序性";内存屏障只是它的**实现手段**之一。没说出 HB 的**规则清单**(面试真正要的)。
补充:八大规则(程序顺序、监视器锁、volatile 写读、线程 start/join、传递性等);"A happens-before B" 的含义是 A 的结果对 B 可见。

完整讲解

第一部分:知识重点剖析

**核心考点定位**:能否说清 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++ 依然线程不安全,该上锁还得上锁。

#2 CountDownLatch、CyclicBarrier、Semaphore区别? low java-concurrency

题目

CountDownLatch、CyclicBarrier、Semaphore区别?

我的回答

三者是AQS中共享模式下的三个不同实现; countDownlatch是用于单个主线程下执行计数递减使用,每个对象只能用一次,所以要设置在主方法内使用,用过归零后就需要重新创建了,比如对资源的调用控制; cyclicbarrier是用于进行线程启动控制,用于等待多个线程到达后再执行下一步操作,可以用来进行结果组合,多线程调用不同下游,然后等待所有下游结果返回后,进行对结果统一组合; semaphore是信号量,控制一次可以通过的线程数,超过的会被阻塞,可以用来做限流,一次只允许一定个数的线程通过执行

点评

优点:三者用途都讲对了——CDL 计数归零、CB 线程到齐、Semaphore 控制通过数量;CDL/Semaphore 属于 AQS 共享模式也对。
不足:**"三者都是 AQS 共享模式"错**——CyclicBarrier 是 `ReentrantLock + Condition` 实现的,不走 AQS。也没点出"一次性 vs 可复用"这个核心对比。
补充:CDL 一次性、CB 可循环复用(有 reset/barrierAction);CDL 等的是"事件",CB 等的是"彼此",Semaphore 控制的是"并发许可数"。

完整讲解

第一部分:知识重点剖析

**核心考点定位**:三者的语义差异 + 底层实现方式。

**知识点拆解**:
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。

#3 什么是线程池,如何实现的? low java-concurrency

题目

什么是线程池,如何实现的?

我的回答

线程池从字面上它是线程的存储池,它是一种池化技术。池化技术本身核心的目的就是对相对耗资源或者建立耗资源的数据资源资源物进行一个缓存,达到重复利用利用资源的目的。线程池的实现本身它是把一些线程先创建出来,然后给,在你使用线程的时候直接从池内获取,达到不让你每次去建立一个新线程的目的。然后这个线程使用了之后,你再把这个线程退回给线程池,后续其他的主线程在使用的时候再从线程池里拿。最终实现线程的复用。然后线程池这以 Java 为主去说吧。Java 的线程池主要定义了几个核心参数。首先是核心线程数,也就是最初这个线程池内会保留常驻的线程会有多少。基本上在你创建了请求过来之后,它就会从常驻的核心线程里边拿数据。然后其次它有最大线程,最大的线程数,也就是说这个线程,线程池,当线程任务比较多的时候,会最大扩展,扩展到多少个线程。当扩展到这个线程之后,是不会再去继续扩展的了,这也就是最大线程数。然后是另外的核心参数,就是任务队列。这个是当你请求的任务线程,核心线程已经占用满了之后,有多少任务存留下来。暂缓放到队列里边,然后等核心线程处理完其他的事务之后,再去从任务队列里拿,然后进行一个交替地去获取任务去执行的一个过程。然后这个过程中需要使用任务队列,这个任务队列就是这个一个核心参数,它里边存放着一些要处理的任务。当任务队列满了之后,才开始扩展非核心线程,去使用后续的那个最大线程数去扩展线程,非核心线程。然后另外的就是拒绝策略,比较核心的拒绝策略,这个拒绝策略主要应对的目的就是,当线程池实在处理不了你这个线程了,实在处理不了你的任务的时候,以什么方式去给你反馈。常见的是直抛异常,就是你你给我发任务,我处理不了了,我就抛一个异常给你,你自己想办法处理,告诉你出问题了。还有就是吞掉,什么都不处理,直接就过掉,把这个任务放弃。另外是可以自定义,还有就是把这个任务放给当前调用的主线程去处理,它自己处理,我不处理。基本上就这些。

点评

优点:- 池化思想理解到位,讲清楚了"复用线程"这个本质 - 核心参数关系讲对了:核心线程 → 队列 → 非核心线程 → 拒绝策略 的触发顺序 - 拒绝策略列举了 4 种典型实现(抛异常/吞掉/自定义/主线程执行)
不足:- 参数不全,漏了 `keepAliveTime`(非核心线程存活时间)和 `threadFactory`(线程工厂,用于线程命名,排查问题必备) - 拒绝策略没给具体名称:`AbortPolicy`、`DiscardPolicy`、`DiscardOldestPolicy`、`CallerRunsPolicy` - 没说清 WorkQueue 类型:`ArrayBlockingQueue`、`LinkedBlockingQueue`、`SynchronousQueue`、`PriorityBlockingQueue` - 漏了 `allowCoreThreadTimeOut`、线程池 5 态、`Executors` 禁用原因 - 缺少工程实践:核心参数如何估算、动态调优、监控指标
补充:1. 7 大核心参数:corePoolSize / maximumPoolSize / keepAliveTime / unit / workQueue / threadFactory / handler 2. 执行流程:来任务 → core?→ 队列未满?→ max?→ 拒绝策略(队列优先级高于 max 扩容) 3. 队列选择:CPU 密集用 SynchronousQueue;IO 密集用 ArrayBlockingQueue 有界 4. Executors 风险:LinkedBlockingQueue 无界 OOM、CachedThreadPool 线程暴涨 5. 估算公式:CPU 密集 N_cpu × 1,IO 密集 N_cpu × 2 或 N_cpu × (1 + wait/compute) 6. 监控指标:queueSize / activeCount / completedTaskCount / rejectedCount(MBean 暴露)

完整讲解

> 最后更新:2026-10-26 · 基于第 3 次作答

### L1 · 30 秒速记

> **结论**:线程池 = 池化 + 限流 + 隔离;7 大参数 + 三步执行流程(core → 队列 → max → 拒绝)是核心。
> **关键词**:7 大参数 · core→队列→max→拒绝 · Executors 禁用
> **边界**:队列优先级高于 max 扩容;无界队列 OOM;CallerRunsPolicy 天然限流

### L2 · 核心讲解

**核心考点定位**:能否说清 ThreadPoolExecutor 的参数体系、执行流程、工程选型。

**机制链(3 环)**:
1. 线程创建成本高(栈内存 ~1MB + OS 系统调用) → 频繁创建销毁浪费 → 需要池化复用
2. ThreadPoolExecutor 7 大参数控制复用 + 限流 + 隔离 → corePoolSize 常驻 + workQueue 缓冲 + maximumPoolSize 峰值 + handler 拒绝
3. 执行流程:任务进来 → core 未满就建核心 → 队列未满就入队 → max 未满就建非核心 → 拒绝策略 → **队列优先级高于 max 扩容**(避免频繁创建线程)

**证据锚点**(去重 ≥6):
- corePoolSize:常驻线程数
- maximumPoolSize:最大线程数
- keepAliveTime:非核心线程空闲回收时间
- workQueue:ArrayBlockingQueue / LinkedBlockingQueue / SynchronousQueue / PriorityBlockingQueue
- handler:AbortPolicy / DiscardPolicy / DiscardOldestPolicy / CallerRunsPolicy
- threadFactory:线程工厂(必须命名,排查必备)
- 执行流程:core → 队列 → max → 拒绝
- 参数估算:CPU 密集 N_cpu×1,IO 密集 N_cpu×2

**对比与权衡**:

| 队列类型 | 有界 | 阻塞特性 | 适用场景 |
|---|---|---|---|
| ArrayBlockingQueue | ✅ | 有界 FIFO | IO 密集,控内存 |
| LinkedBlockingQueue | 可无界 | 默认无界 | ❌ OOM 风险 |
| SynchronousQueue | ❌ | 不存储 | CPU 密集 |
| PriorityBlockingQueue | 无界 | 按优先级 | 任务有优先级 |

**权衡**:队列选择要平衡吞吐(有界防 OOM)与延迟(SynchronousQueue 直接传递),生产环境建议 ArrayBlockingQueue 有界 + CallerRunsPolicy 限流。

**常见误区**:
1. 错误:"线程池 7 大参数记不清也没事" → 正确:必须全答——core / max / keepAliveTime / unit / workQueue / threadFactory / handler → 为什么:每个参数都有具体含义和调优方向
2. 错误:"队列满了才扩 max 线程" → 正确:队列优先级高于 max 扩容——队列满才扩,队列未满不会扩 → 为什么:避免频繁创建线程,队列作为缓冲池
3. 错误:"Executors 创建的线程池可以用" → 正确:Executors 全部禁用——内部用无界队列或无限线程,OOM 风险 → 为什么:FixedThreadPool 用 LinkedBlockingQueue 无界;CachedThreadPool 无 max
4. 错误:"threadFactory 不重要" → 正确:必须给线程命名,排查必备(jstack 抓栈能看出业务归属) → 为什么:默认线程名"pool-N-thread-M"无法定位
5. 错误:"参数估算用经验值" → 正确:CPU 密集 N_cpu×1,IO 密集 N_cpu×2 或 N_cpu×(1+wait/compute);再压测调优 → 为什么:不同场景估算公式不同

**适用边界**:
- **适合**:任务提交速率可控、有明确 QPS 预估的场景
- **不适合**:突发流量无上限(有界队列 + 拒绝策略防 OOM);任务耗时未知(监控告警必备)
- **替代方案**:动态线程池(Hippo4j)+ 配置中心推送

### 延伸追问(≥3)

1. **ThreadPoolExecutor.execute 的源码流程?**
- 要点:三关判断(core → queue → max),CAS 修改 workerCount,addWorker 创建线程
- 追问落点:为什么队列优先级高于 max 扩容

2. **为什么队列满了才扩非核心线程?**
- 要点:避免频繁创建销毁线程,队列作为缓冲池;代价是延迟升高
- 追问落点:延迟 vs 吞吐的权衡

3. **线程暴涨如何排查?**
- 要点:jstack 抓栈 → 看线程名 → 定位提交方 → 看 queue 是否无界
- 追问落点:threadFactory 命名的重要性

4. **ForkJoinPool 和 ThreadPoolExecutor 区别?**
- 要点:ForkJoin 用工作窃取,无界任务队列,适合并行任务;TP 适合独立任务
- 追问落点:分治任务如何选

5. **动态线程池如何落地?**
- 要点:阿里 Hippo4j / 美团动态线程池;配置中心推送 + JMX 回调 + 告警
- 追问落点:生产环境调优实践

### 面试者参考回答(400–700 字)

**【结论】** 线程池本质是池化思想,核心目标是复用线程 + 限流 + 隔离。

**【三点】**
第一,**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 密集)做初值,再压测调优。

---

#4 创建线程有几种方式? medium java-concurrency

题目

创建线程有几种方式?

我的回答

java创建线程可以通过thread来直接new创建,也可以通过实现runnable接口来实现,或者是从线程池中取用线程来实现,所有线程创建回收等操作由线程池来管控; 常规情况主要使用线程池来进行线程管控,可以更好的利用线程资源;

点评

优点:答到 Thread 子类、Runnable、线程池三种,并指出工程上首选线程池。
不足:**漏了 Callable + FutureTask(带返回值)** 这条最常考的方式;也没说清 Runnable 与 Callable 的区别;没说"线程池本质也是复用 Thread/Runnable"。
补充:标准 4 种——继承 Thread、实现 Runnable、实现 Callable(配 FutureTask)、线程池(ExecutorService);Java 单继承限制决定了"实现接口"优于"继承 Thread"。

完整讲解

第一部分:知识重点剖析

**核心考点定位**:能不能列出 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` 快捷工厂。

#5 Java是如何判断一个线程是否存活的? medium java-concurrency

题目

Java是如何判断一个线程是否存活的?

我的回答

java中判断线程是否存活可以通过线程池提供的一个方法来判定,该方法是通过判定线程当前状态来识别线程池存活的,线程状态不为new和结束的都算存储

点评

优点:状态判断逻辑对——**非 NEW 且非 TERMINATED 即存活**,这是核心。
不足:**API 归属说错**——是 `Thread.isAlive()`,不是"线程池提供的方法";没点名 `getState()`。对比首次(2026-09-29,3/5)在 API 精确性上**退步**。
补充:`isAlive()` ≈ `state != NEW && state != TERMINATED`;存活 ≠ 运行——BLOCKED/WAITING/TIMED_WAITING 都算存活。 > ℹ️ 完整讲解见上方首次记录(2026-09-29)

完整讲解

第一部分:知识重点剖析

**核心考点定位**:考察对 `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 状态字段,无锁读取

第三部分:面试者参考回答(300-600 字口语化)

面试官您好,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 抛异常;线程管理框架在补充新线程前也会检查旧线程是否还在。

---

#6 synchronized的锁升级过程是怎样的? medium java-concurrency

题目

synchronized的锁升级过程是怎样的?

我的回答

synchronize,在jdk1.6之后增加了锁升级的逻辑,整个升级过程是根据锁的对象被访问的线程频率来的。当没有线程访问的时候是无锁的; 当有单个线程访问,没有并发线程进入的时候,是偏向锁,在线程头部记录当前线程,如果一致则直接访问不需要做什么控制判断; 当有多个线程访问,但是竞争度不高的时候使用的是轻量级锁,或者说自旋锁,持有锁的线程执行过程中,其他线程在自循环,等待持有者释放锁,释放后新线程直接持有锁,其他线程继续自旋,当自旋到一定次数还没有获取锁后就会进行锁升级,转为重量级锁,这时候直接是使用操作系统底层的锁机制来进行线程控制,持有锁的线程正常执行,其他线程进入阻塞状态,等待持有锁线程执行完毕唤醒所有阻塞中的线程争抢锁的持有状态

点评

优点:**四态路径完整**(无锁→偏向→轻量级→重量级)、触发条件准确(单线程→偏向、多线程低竞争→轻量级、自旋超阈值→重量级)、"对象头记录线程 ID"和"重量级用 OS 挂起"都讲到了;比首次(2026-09-29,3/5)更有条理。
不足:版本事实仍错("JDK 1.6 之后"——实际 **1.5 引入**、**JDK 15 默认关偏向**、**JDK 18 移除**);漏 Lock Record 结构、锁消除/锁粗化、"升级不可逆"的准确表述。
补充:现代 JDK(16+)实际只有"无锁 → 轻量级 → 重量级"三态。 > ℹ️ 完整讲解见上方首次记录(2026-09-29)

完整讲解

第一部分:知识重点剖析

**核心考点定位**:考察对 `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

第三部分:面试者参考回答(300-600 字口语化)

面试官您好,`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。

---

#7 如何实现无锁化编程? medium java-concurrency

题目

如何实现无锁化编程?

我的回答

无锁化编程方面有多种实现方式;首先可以规避共享对象的变更,没有多线程共享的资源也就没有锁的必要了;相关的有 ThreadLocal 的方案,和所有对象都在线程栈内部不做状态共享;另外就是通过 CAS 使用比较交换的方式,验证当前要变动更新的资源是我预期的资源,而没被其他线程污染,也可以实现无锁化的操作,不过多数这种只应对一个字段;另外就是原子命令,这个命令执行就一下不会有线程能插入进来,比如原子类的各种加减操作。

点评

优点:- **三条主线答全**(避免共享状态 / CAS / 原子类)——方向完整 - 意识到 CAS 只应对单字段——**边界认知对** - 提到 ThreadLocal + 栈封闭——**无共享状态思路对**
不足:- **完全漏 ABA 问题**——这是 CAS 最重要的边界(`AtomicStampedReference` 版本号解法) - **漏 LongAdder**——高并发计数场景的核心手段 - **漏 Unsafe / VarHandle**——底层 API,考察"是否看过源码" - **漏自旋开销**——竞争激烈时 CAS 反而浪费 CPU - 未提**复合操作的解决方案**(`AtomicReference` 打包)

完整讲解

> 最后更新:2026-10-26 · 基于第 2 次作答

L1 · 30 秒速记

> **结论**:无锁化 = **CAS + 原子类 + 无共享状态** 三条主线;核心是乐观重试 + 原子指令替代阻塞锁。
> **关键词**:CAS cmpxchg · LongAdder 分段 · AtomicStampedReference 解 ABA
> **边界**:只保单变量原子性;ABA 需版本号;高竞争自旋反而浪费 CPU

L2 · 核心讲解

**核心考点定位**:无锁的手段清单 + 各自边界 + ABA 治理。

**机制链(3 环)**:
1. 传统锁(synchronized/ReentrantLock)→ 阻塞等待 → CPU 上下文切换开销高 → **在高并发、低竞争场景效率低**
2. 无锁化思路 = 乐观重试 + 原子指令 → **CAS(compare-and-swap)用 CPU 的 `cmpxchg` 指令原子完成"读-比-写"** → 失败重试而非阻塞
3. 但 CAS 有 ABA 问题(值 A→B→A 后 CAS 误判)→ 用 `AtomicStampedReference` 加版本号解决 → 高竞争时自旋浪费 CPU → 改用 `LongAdder` 分段或锁

**证据锚点**(去重 ≥10):
- `Unsafe.compareAndSwapInt`:CAS 底层实现
- `cmpxchg + lock`:x86 指令,保证原子
- `AtomicInteger / AtomicLong / AtomicReference`:原子类 API
- `AtomicStampedReference`:ABA 解法,版本号
- `AtomicMarkableReference`:ABA 简化解法,布尔标记
- `LongAdder`:分段 Cell 计数,高并发优于 AtomicLong
- `VarHandle`:Java 9+ 现代 CAS API(替代 Unsafe)
- `ThreadLocal`:无共享状态方案
- `final` + 构造期完成:不可变对象
- 自旋开销:高竞争时 CAS 反而比阻塞锁慢

**对比与权衡**:

| 方案 | 原子性 | 竞争开销 | ABA 防护 | 适用 |
|---|---|---|---|---|
| synchronized / ReentrantLock | ✅ | 阻塞,高竞争 O(n) | 不需要 | 高竞争、复合操作 |
| CAS(AtomicXxx) | ✅(单变量) | 低竞争无自旋 | ❌ | 单变量低竞争 |
| AtomicStampedReference | ✅(单变量) | 低竞争无自旋 | ✅ | 单变量需 ABA 防护 |
| LongAdder | ✅(弱一致 sum) | 分段无自旋 | N/A | 高并发计数 |
| volatile(读写) | 不保证原子 | 内存屏障开销 | N/A | 可见性 |

**常见误区**:
1. 错误:"CAS 一定比锁快" → 正确:低竞争时快,高竞争时自旋反而浪费 CPU,不如锁 → 为什么:CAS 是乐观重试,竞争大时重试次数暴涨
2. 错误:"CAS 能解决所有原子性问题" → 正确:只保单变量,复合操作要 `AtomicReference` 打包或直接加锁 → 为什么:两个变量的原子性无法用 CAS 单独保证
3. 错误:"ABA 问题很少出现" → 正确:链表节点复用、版本号更新等场景常见 → 为什么:ABA 隐蔽,容易被误判为幂等
4. 错误:"LongAdder 和 AtomicLong 完全一样" → 正确:LongAdder 分段 Cell 分散热点,sum() 是弱一致;AtomicLong 是全局 CAS → 为什么:高并发计数用 LongAdder 吞吐高得多
5. 错误:"ThreadLocal 就是无锁" → 正确:ThreadLocal 是"无共享状态",不是无锁化手段本身 → 为什么:无锁化是 CAS 类,ThreadLocal 是避开共享

**适用边界**:
- **适合**:单变量低竞争更新(计数器、状态标志);读多写少场景
- **不适合**:高竞争场景(LongAdder 替代);复合操作(AtomicReference 或锁);需要严格一致性的 sum(AtomicLong)
- **替代方案**:高竞争计数用 LongAdder;复合操作用 ReentrantLock;虚拟线程下用 CAS 避免 pinning

### 延伸追问(≥3)

1. **CAS 的 ABA 怎么解决?**
- 要点:`AtomicStampedReference` 加版本号;`AtomicMarkableReference` 加布尔标记
- 追问落点:ABA 实际业务场景(链表节点复用)

2. **LongAdder 为什么比 AtomicLong 快?**
- 要点:分段 Cell 分散热点,减少 CAS 竞争;`sum()` 是弱一致
- 追问落点:LongAdder 与 AtomicLong 的选型决策

3. **CAS 一定自旋吗?竞争大怎么办?**
- 要点:不一定,失败就重试;高竞争时自旋浪费 CPU,改用 LongAdder 或加锁
- 追问落点:自适应自旋(AQS 里的 `LockSupport.park`)

4. **Unsafe 和 VarHandle 有什么区别?**
- 要点:VarHandle 是 Java 9+ 现代 API,有类型检查和访问控制;Unsafe 已 @Deprecated(Java 17+)
- 追问落点:迁移路径

### 面试者参考回答(400–700 字)

**【结论】** 无锁化编程本质是"用乐观重试 + 原子指令替代阻塞锁",三条主线:CAS + 原子类 + 无共享状态。

**【三点】**
第一,核心手段是 **CAS 和原子类**,CAS 靠 CPU 的 `cmpxchg` 加锁前缀保证原子,失败就自旋重试。它解决的是**单变量**的原子更新,复合操作要用 `AtomicReference` 打包。

第二,**ABA 是 CAS 最大的坑**——值 A→B→A 后 CAS 会误判为未变。用 `AtomicStampedReference` 加版本号解决,`AtomicMarkableReference` 用布尔标记。高并发计数场景用 `LongAdder`,它分段 Cell 分散热点,比 `AtomicLong` 快得多(`sum()` 是弱一致)。

第三,还有**无共享状态**的思路——不可变对象、ThreadLocal,从根上避开竞争。

**【边界】** 三点提醒:一是 CAS 只保单变量原子性,复合操作要 `AtomicReference` 或加锁;二是高竞争时 CAS 自旋反而浪费 CPU,不如 LongAdder 或退回到锁;三是 ABA 隐蔽,链表复用等场景常见,必须用版本号防护。

#8 synchronized是如何保证原子性、可见性、有序性的? high java-concurrency

题目

synchronized是如何保证原子性、可见性、有序性的?

我的回答

synchronized 在重量级锁的时候,通过操作系统底层指令来实现原子性、可见性、有序性的;其中原子性是通过 lock 命令和 unlock 命令来控制,临界范围内的代码是不会被其他线程介入,其他线程会被操作系统阻塞等待 unlock 命令执行完成后才会去争抢锁持有;可见性是在锁退出的时候会刷新主存,让其他线程可见;有序性是依据 happend-before 规则来实现的。

点评

优点:- 三机制映射方向对(原子性=lock/unlock、可见性=刷主存、有序性=happens-before) - 提到其他线程会阻塞等待——意识对

完整讲解

> 最后更新:2026-10-26 · 基于第 3 次作答

### L1 · 30 秒速记

> **结论**:synchronized 通过 Monitor + 内存屏障 + JIT 禁重排,同时保证原子性、可见性、有序性;是 volatile 的超集。
> **关键词**:Monitor owner · 解锁刷主存 + 加锁 reload · happens-before 第 17 条
> **边界**:synchronized 是非公平锁;volatile 只保可见+有序,不保原子性

### L2 · 核心讲解

**核心考点定位**:能否说清"原子性 = 互斥"、"可见性 = 解锁刷内存"、"有序性 = 禁止指令重排"这三条底层机制,以及 happens-before 规则。

**机制链(4 环)**:
1. 并发三大问题(原子性/可见性/有序性)→ 单一手段难以同时解决 → 需要复合机制
2. synchronized 原子性 ← Monitor 互斥:每个对象隐含 Monitor,含 owner 指针;线程 CAS 抢 owner 成功后独占执行 → 其他线程进不来
3. 可见性 ← 解锁刷主存 + 加锁 reload:解锁时 StoreLoad 屏障强制刷回主存,加锁时强制从主存 reload → 加锁 reload 是常被漏掉的一半
4. 有序性 ← JIT 禁重排 + 内存屏障:synchronized 块内指令不被重排到块外,CPU 层 StoreLoad 屏障保跨块顺序
5. JMM 第 17 条 happens-before:解锁 → 后续加锁 → 是"可见性 + 有序性"的契约基础

**证据锚点**(去重 ≥12):
- Monitor:每个对象隐含的管程,含 owner 指针
- CAS 抢 owner:进入 synchronized 块的底层动作
- entryList:重量级锁下阻塞等待的线程队列
- 自旋 CAS:轻量级锁下的竞争方式
- StoreLoad 屏障:解锁时刷主存
- 强制 reload:加锁时丢弃工作内存
- happens-before 第 17 条:解锁 → 加锁
- JIT 禁重排:synchronized 块内指令不被重排到块外
- 非公平锁:synchronized 不保证先到先得
- volatile 对比:可见性 + 有序性,无原子性
- AtomicXxx 对比:CAS 保证原子性,无可见性屏障
- ReentrantLock(true):公平锁替代
- Mark Word:对象头 64 位 8 字节,含锁标志 + owner + hash code + GC 年龄
- 锁升级:无锁 → 偏向 → 轻量 → 重量(JDK 15 默认关闭偏向锁)
- DCL 单例:volatile + 双重检查的正确写法

**对比与权衡**:

| 关键字 | 原子性 | 可见性 | 有序性 | 实现机制 | 开销 |
|---|---|---|---|---|---|
| volatile | ❌ | ✅ | ✅ | 内存屏障 | 中 |
| synchronized | ✅ | ✅ | ✅ | Monitor + 内存屏障 + JIT 禁重排 | 高 |
| AtomicXxx(CAS) | ✅ | ✅(volatile 语义) | ✅ | CPU cmpxchg | 低 |
| ReentrantLock | ✅ | ✅ | ✅ | AQS(CLH 队列) | 中 |

**权衡**:synchronized 是"三合一"的最强保证,但开销最大;高竞争场景应退回到 Lock / CAS / 分段方案。

**常见误区**:
1. 错误:"synchronized 是公平锁" → 正确:synchronized 是**非公平锁**,JVM 内部不保证先到先得 → 为什么:需要公平性用 `ReentrantLock(true)`
2. 错误:"有序性指线程获取锁的顺序" → 正确:有序性指**指令级重排**——JIT 不重排 synchronized 块内指令 → 为什么:JMM 的有序性是 JIT 优化边界,不是线程调度
3. 错误:"synchronized 只保证原子性" → 正确:synchronized 同时保证原子性 + 可见性 + 有序性 → 为什么:Monitor 机制天然包含三者
4. 错误:"可见性靠解锁刷主存就够了" → 正确:可见性靠解锁刷主存 + **加锁时 reload**——两半缺一不可 → 为什么:加锁时不 reload 可能读到旧值
5. 错误:"volatile 能保证原子性" → 正确:volatile 只保可见性 + 有序性,不保原子性(`i++` 三步仍会交错) → 为什么:volatile 语义边界是"单变量一次读写"

**适用边界**:
- **适合**:短临界区、读多写少、需要复合操作原子性
- **不适合**:高竞争场景(上下文切换开销大);长临界区(含 IO/计算)
- **替代方案**:高竞争用锁分段 / StampedLock / LongAdder;无锁场景用 CAS

### 延伸追问(≥3)

1. **synchronized 能保证原子性,为什么不用 volatile 也能保证可见性和有序性?**
- 要点:volatile 加内存屏障保证可见+有序,但读-改-写三步仍会交错;synchronized 用互斥把三步包成一个原子块
- 追问落点:volatile vs synchronized 的选择决策

2. **synchronized 是公平锁吗?为什么说是非公平锁?**
- 要点:非公平锁。JVM 内部不保证先到先得;多个线程竞争时随机调度。需要公平性时用 `ReentrantLock(true)`
- 追问落点:公平锁 vs 非公平锁的性能权衡

3. **happens-before 有哪些规则?**
- 要点:7 条(程序顺序、监视器锁、volatile 变量、线程启动、线程终止、线程中断、对象终结);是 JMM 保证并发正确性的最小契约
- 追问落点:与 volatile / synchronized 的对应关系

4. **Monitor 的 owner 指针在对象头的什么位置?**
- 要点:对象头 Mark Word(64 位 8 字节,32 位 4 字节),存储锁标志、owner、hash code、GC 年龄
- 追问落点:锁升级(无锁→偏向→轻量→重量)

5. **volatile 和 synchronized 能同时用吗?**
- 要点:可以同时用,但 volatile 通常足够时不需要 synchronized;volatile 用于简单标志位,synchronized 用于复合操作
- 追问落点:实际代码里的组合使用

### 面试者参考回答(400–700 字)

**【结论】** synchronized 同时保证三个特性:**原子性、可见性、有序性**,但每一条的底层机制不同。

**【三点】**
第一,**原子性 = Monitor 互斥**。每个对象隐含一个 Monitor,含 owner 指针。线程进入 synchronized 块时,先尝试用 CAS 把 owner 设成自己;成功了独占执行,失败了在队列里等。重量级锁状态下其他线程挂起到 entryList 阻塞等待,轻量级锁下通过自旋 CAS 竞争。所以从"互斥"角度看,原子性其实是锁的基本性质——你进得去,别人就进不来。

第二,**可见性 = 解锁刷内存 + 加锁 reload**。解锁时把工作内存里的修改强制刷回主存,并加 StoreLoad 内存屏障;加锁时丢弃本地工作内存,强制从主存 reload 最新值。JMM 第 17 条规则总结:对一个 synchronized 块的解锁,happens-before 后续对同一把锁的加锁。这是并发编程可见性的契约基础。

第三,**有序性 = 禁止指令重排**。这里的有序性不是"线程获取锁的顺序",而是**指令级重排**——JIT 编译器不会把 synchronized 块内部的指令重排到块外部。CPU 层面的 StoreLoad 屏障也保证了跨块的指令顺序。比如 DCL 单例不加 volatile,`new` 指令会被重排成"分配内存→赋引用→初始化构造器",其他线程可能看到半构造实例。

**【边界】** 对比 volatile:volatile 只保可见性和有序性,不保原子性(`i++` 三步仍会交错);synchronized 通过互斥把三步包成一个整体,所以三个特性都保。但 synchronized 开销更大,高竞争场景要慎重。另外提醒:**synchronized 是非公平锁**,需要公平性用 `ReentrantLock(true)`。

---

#9 高并发下 synchronized 竞争激烈导致线程上下文切换开销过大,如何定位并优化? high java-concurrency

题目

高并发下 synchronized 竞争激烈导致线程上下文切换开销过大,如何定位并优化?

我的回答

高并发下 synchronized 竞争激烈导致上下文切换开销过大,可以检查下是否是进入的线程太多了;是否可以控制不让线程进入;定位的话可以查看下活跃线程情况,是否有大量线程阻塞,使用 jstack 导出线程栈信息看是否都是某个方法的线程阻塞,如果是基本上是这个方法的锁并发有问题;处理方案方面可以考虑是否锁粒度太大,做粒度缩减;是否是进入线程过多,在前置做判定拦截,走缓存,反正不要走到锁这里,锁的资源是宝贵的,不要浪费;看是否能转成无锁化编程,用其他方式代替。

点评

优点:- 定位思路(jstack 看阻塞 + 线程栈)方向对 - 归因(并发高 / 锁粒度大)对 - 优化方向(缩小锁范围 / 缓存前置 / 无锁化)方向对 - "锁的资源宝贵"表达——**权衡意识对**
不足:- **完全漏量化定位工具**——`pidstat -w`(cswch/nvcswch)、`vmstat cs`、JFR `jdk.JavaMonitorBlocked`、火焰图 "Java Monitor Blocked" - **优化手段太笼统**——未提具体方案:锁分段(Lock Striping)、读写锁/StampedLock、LongAdder、ConcurrentHashMap - 未提**去锁前的判断**:如何确认瓶颈是锁还是 CPU 计算(BLOCKED 比例高 + cswch 高 → 锁;RUNNABLE 高 + CPU 打满 → 计算) - 未提**上游限流**、**背压**(如 CallerRunsPolicy)

完整讲解

> 最后更新:2026-10-26 · 基于第 2 次作答

### L1 · 30 秒速记

> **结论**:先用数据定位(jstack + pidstat + 火焰图),确认是锁竞争而非 CPU 计算;再分层优化(缩小临界区 → 分段 → 换锁 → 去锁)。
> **关键词**:pidstat -w cswch · JFR jdk.JavaMonitorBlocked · 锁分段 / StampedLock / LongAdder
> **边界**:上下文切换不一定是坏事;语义正确性优先于性能

### L2 · 核心讲解

**核心考点定位**:能否用数据定位锁竞争/上下文切换,并给出分层优化手段。

**机制链(4 环)**:
1. 现象:CPU 高但业务 QPS 不涨 → 上下文切换过多 → 需要区分锁竞争 vs CPU 计算
2. 定位:jstack 看 BLOCKED 比例 + `pidstat -w` 看 cswch/nvcswch + JFR `jdk.JavaMonitorBlocked` → 确认热点锁
3. 归因:并发线程数远超 CPU 核数(调度切换);临界区太长(持锁时间久);锁粒度过粗
4. 优化分层:减小锁范围 → 锁分段降粒度 → 读多写少换 StampedLock/读写锁 → 高竞争计数用 LongAdder → CAS/异步彻底去锁

**证据锚点**(去重 ≥12):
- `jstack <pid>`:看 `java.lang.Thread.State: BLOCKED` 分布
- `pidstat -w <pid> 1`:cswch(自愿切换)/ nvcswch(非自愿切换)
- `vmstat 1` cs 列:上下文切换次数
- JFR `jdk.JavaMonitorBlocked`:Monitor 阻塞事件
- JFR `jdk.JavaThreadBlocked`:线程阻塞事件
- 火焰图:async-profiler / Arthas `profiler start --event cpu`
- `-XX:+PrintLocks`:JVM 参数打印锁信息
- 锁分段:ConcurrentHashMap 1.7 的 Segment 思路
- StampedLock:乐观读 + 悲观读写锁
- LongAdder:分段 Cell 计数,高并发优于 AtomicLong
- ConcurrentHashMap:分段 + CAS 无锁化
- 上游限流:Sentinel / Resilience4j 熔断器
- 背压:CallerRunsPolicy 让上游感受慢

**对比与权衡**:

| 优化手段 | 适用场景 | 代价 | 复杂度 |
|---|---|---|---|
| 缩小临界区 | 所有场景 | 需重构 | 低 |
| 锁分段 | 高并发热点锁 | 内存 +1 | 中 |
| StampedLock | 读多写少 | 乐观读失败需重试 | 中 |
| 读写锁 | 读多写少 | 写锁阻塞读 | 中 |
| LongAdder | 高并发计数 | sum 弱一致 | 低 |
| CAS + AtomicReference | 单变量低竞争 | ABA 风险 | 中 |
| 上游限流 | 流量源头 | 拒流量 | 中 |

**权衡**:先数据定位再动手,避免盲目"去锁";上下文切换高不一定坏——要确认它真的没带来收益。工程实践中我一般按"定位→归因→分层优化"三步走:先花 30 分钟确认瓶颈在锁不在 CPU,再判断是粒度问题还是数量问题,最后选合适的优化手段。这样避免"上线后没效果、白折腾"的情况。特别要注意的是,很多所谓"锁竞争"其实是把 CPU 密集任务包装成"锁"造成的假象,先确认业务逻辑,再动锁。

**常见误区**:
1. 错误:"上下文切换高一定是问题" → 正确:上下文切换高不一定是坏事,要看是否带来业务收益 → 为什么:合理的切换是正常调度
2. 错误:"看到 BLOCKED 多就一定是锁竞争" → 正确:要先看 `pidstat -w` cswch + JFR 才能确认 → 为什么:可能是 GC 暂停或其他原因
3. 错误:"去锁 = 加 CAS" → 正确:CAS 只适合单变量低竞争;复合操作、高竞争要退回到锁或分段 → 为什么:无锁化有边界
4. 错误:"缩小临界区就是好事" → 正确:缩小临界区要确保语义正确,不能为了性能破坏原子性 → 为什么:正确性优先于性能
5. 错误:"高并发下 StampedLock 一定比读写锁好" → 正确:StampedLock 乐观读失败需重试,写锁阻塞所有读;高竞争写场景反而不如读写锁 → 为什么:不同场景适配不同锁
6. 错误:"锁分段越多越好" → 正确:分段太多会带来内存开销和锁管理成本;一般按 CPU 核数或业务 key 数量确定 → 为什么:分段是收益和成本的权衡
7. 错误:"优化锁就完了,不用改业务" → 正确:如果锁保护的是 CPU 密集逻辑,考虑改成异步/预计算,从根本上减少竞争 → 为什么:优化锁只是治标,改业务是治本

**适用边界**:
- **适合**:CPU 高但业务 QPS 不涨;jstack 大量 BLOCKED;pidstat cswch 高
- **不适合**:CPU 高且业务 QPS 正常(是 CPU 计算瓶颈,不是锁);BLOCKED 少(可能是 IO 阻塞或 GC)
- **替代方案**:先定位再优化,避免盲目"去锁"

### 延伸追问(≥3)

1. **怎么确认瓶颈是锁竞争而不是 CPU 计算?**
- 要点:BLOCKED 比例高 + cswch 高 → 锁竞争;RUNNABLE 高且 CPU 打满 → CPU 计算瓶颈
- 追问落点:如何用 JFR 组合判断

2. **锁分段怎么实现?**
- 要点:把一个锁拆成 N 个按 key 取模,降低竞争(ConcurrentHashMap 1.7 思路)
- 追问落点:分段数怎么定?2 的幂?

3. **自旋锁在什么场景反而更差?**
- 要点:竞争激烈时自旋空耗 CPU,不如直接阻塞挂起
- 追问落点:AQS 的自适应自旋机制

4. **高并发计数用什么?**
- 要点:LongAdder(分段 Cell)优于 AtomicLong;sum() 是弱一致
- 追问落点:LongAdder 内部结构

5. **如何建立锁竞争监控告警?**
- 要点:JFR `jdk.JavaMonitorBlocked` 事件 + Prometheus 告警;JVM 参数 `-XX:+PrintLocks`
- 追问落点:Prometheus + JFR 组合方案

### 面试者参考回答(400–700 字)

**【结论】** 我会先用数据确认瓶颈在锁竞争,再做分层优化。

**【三点】**
第一,**定位**:`jstack` 看大量 BLOCKED、`pidstat -w` 看每秒上下文切换(cswch 自愿 / nvcswch 非自愿)、`vmstat 1` 的 cs 列、JFR `jdk.JavaMonitorBlocked` 事件、火焰图看 monitor blocked。确认热点锁后再动手。

第二,**归因**:要么并发线程数远超 CPU 核数(调度切换开销),要么临界区太长(持锁时间久导致排队),要么锁粒度过粗(一个锁保护过多资源)。三种情况对应不同优化方向。

第三,**优化分层**:先**缩小临界区**(去掉临界区里的 IO/计算,只锁必要代码);再**锁分段降粒度**(一个锁拆 N 个按 key 取模,ConcurrentHashMap 1.7 思路);**读多写少换 StampedLock/读写锁**;**高竞争计数用 LongAdder**(分段 Cell 分散热点);**彻底去锁**用 CAS + AtomicReference 或异步/缓存;**上游限流**用 Sentinel 或 CallerRunsPolicy 让上游感受慢。

**【边界】** 两点提醒:一是上下文切换不一定是坏事——要确认它真的没带来收益;二是不能盲目"去锁",语义正确性优先于性能。先数据定位再动手。

---

#10 如何借鉴 Disruptor 的 RingBuffer 思想设计一个低延迟的无锁并发队列? high java-concurrency

题目

如何借鉴 Disruptor 的 RingBuffer 思想设计一个低延迟的无锁并发队列?

我的回答

首先明确下 ringBuffer 是什么核心是什么;其核心是环形数组配合序列号,通过单生产者、多消费者和 CAS 实现的缓存队列;参考 ringBuffer 的几个核心机制:先定义一个固定长度的环形数组;然后定义生产者写入的位置和消费者消费的位置,多消费者的话应该有一个消费者位置数组;当生产者位置=消费者位置时证明消息队列是空的了;如果生产者位置-最慢消费者消费位置=数组长度,则队列已经满了,不能再写入了;更新消费序号的时候使用 CAS 的方式做操作。

点评

优点:- **环形数组 + 序列号 + CAS** 核心机制答对 - **多消费者位置数组**——意识对 - **空/满判定**逻辑清晰(生产者=消费者 空;差=长度 满)——**方向对**
不足:- **完全漏 Disruptor 的低延迟灵魂**——这是最关键的差距: 1. **容量取 2 的幂**——`index & (size-1)` 位运算代替取模 2. **缓存行填充(padding)** 消除伪共享——序号字段必须 padding 到 64 字节 3. **元素预分配**——启动时全部创建,运行期不 new,零 GC 4. **SequenceBarrier 依赖等待**——消费者按依赖判断可消费位置 5. **批处理**——一次取连续区间,减少 CAS 和等待次数 - 未提**多生产者 CAS 抢占 cursor**的具体机制 - 未提**与 BlockingQueue / ConcurrentLinkedQueue 的性能对比**

完整讲解

> 最后更新:2026-10-26 · 基于第 2 次作答

### L1 · 30 秒速记

> **结论**:Disruptor 低延迟 = **环形数组 + 2 的幂 + 缓存行填充 + 元素预分配 + 批处理** 五件套;用 SequenceBarrier 协调多消费者。
> **关键词**:2 的幂取模优化 · 缓存行 padding 防伪共享 · 预分配零 GC · SequenceBarrier 依赖等待 · 批处理
> **边界**:适合吞吐极高、依赖简单的场景;普通异步解耦用 BlockingQueue 更简单

### L2 · 核心讲解

**核心考点定位**:能否说清 Disruptor 相对于普通 BlockingQueue 的**低延迟工程手段**——不只是"用 CAS 加环形数组"这么粗。

**机制链(4 环)**:
1. 普通 BlockingQueue(ArrayBlockingQueue)→ 内部用 ReentrantLock 互斥 → 高竞争时锁开销大 → **不适合极低延迟场景**
2. Disruptor 用环形数组 + 序列号 → 无锁 CAS 更新 cursor → 只是把锁去掉了,还没到"低延迟"
3. 低延迟的**真正秘密**在于 → 容量取 2 的幂 → 位运算替代取模 → **快 10-100 倍**
4. 缓存行 padding + 元素预分配 + SequenceBarrier 依赖等待 + 批处理 → 消除伪共享、零 GC、减少 CAS 次数 → **亚微秒级延迟**
5. 综合来看 → Disruptor 在极高频、依赖简单的场景下做到低延迟 → 复杂业务、变长消息仍不适合

**证据锚点**(去重 ≥12):
- Disruptor RingBuffer:核心数据结构,固定长度环形数组
- 容量 2 的幂:`1<<n`;`index & (size-1)` 位运算代替 `%`
- Cursor:生产者写入位置,单生产者自增,多生产者 CAS 抢占
- Sequence:消费者位置,多消费者有位置数组
- SequenceBarrier:消费者依赖等待机制,判断可消费位置
- 缓存行 padding:L1 cache line 64 字节,padding 消除伪共享
- `-XX:TargetPredictedSpinning`:JVM 自旋次数参数
- `@sun.misc.Contended`:JDK 9+ 编译期 padding 注解
- 元素预分配:RingBuffer 启动时全部创建 EventObject,运行期不 new
- 批处理(Batch):一次取连续区间 [seqFrom, seqTo],减少 CAS 和等待
- 空/满判定:`cursor == slowestConsumerSequence` 空;`cursor - slowest = size` 满
- BlockingQueue 对比:ArrayBlockingQueue 用 ReentrantLock,非无锁
- ConcurrentLinkedQueue:无锁但节点预分配无、非环形、有 GC 压力
- 伪共享(False Sharing):不同线程修改同一 cache line 内相邻字段导致 cache 频繁失效

**对比与权衡**:

| 队列实现 | 底层同步 | 容量 | 元素创建 | 延迟量级 | 复杂度 |
|---|---|---|---|---|---|
| ArrayBlockingQueue | ReentrantLock | 有界 | 运行时 new | 微秒级(锁开销) | 低 |
| LinkedBlockingQueue | ReentrantLock | 可无界 | 运行时 new | 微秒级 | 低 |
| ConcurrentLinkedQueue | CAS | 无界 | 运行时 new | 微秒级 | 中 |
| Disruptor RingBuffer | CAS + SequenceBarrier | 2 的幂 | 预分配 | 亚微秒级 | 高 |

**常见误区**:
1. 错误:"Disruptor 就是 CAS 加环形数组" → 正确:环形数组 + CAS 只是基础;**2 的幂 / padding / 预分配 / 批处理 / SequenceBarrier** 才是低延迟灵魂 → 为什么:只学 CAS 用不出 5-10 倍的性能优势
2. 错误:"容量必须是 2 的幂" → 正确:Disruptor 强制 2 的幂,为了 `index & (size-1)` 位运算优化;普通环形队列可以用取模 → 为什么:`%` 是除法指令,慢 10-100 倍
3. 错误:"环形数组和数组取模是一回事" → 正确:环形数组的索引是序列号对容量取模,取模是普通操作;Disruptor 用位运算替代取模是因为容量取 2 的幂 → 为什么:位运算优化是 Disruptor 精髓之一
4. 错误:"伪共享不影响 Java 性能" → 正确:Java 对象默认不 padding,多个频繁修改字段可能落到同一 cache line → 触发跨核 cache 失效,性能损失巨大 → 为什么:JMM 不保证对象字段布局
5. 错误:"Disruptor 适合所有场景" → 正确:Disruptor 适合高频、依赖简单、消息长度固定的场景(如行情数据);复杂业务逻辑、变长消息用 MQ 或 BlockingQueue 更合适 → 为什么:Disruptor 学习成本高、灵活性低

**适用边界**:
- **适合**:高频交易、行情分发、日志流处理、微秒级延迟要求;生产者消费者依赖简单
- **不适合**:变长消息、复杂依赖、需要持久化、需要按业务优先级排序的场景
- **替代方案**:普通业务解耦用 BlockingQueue / Kafka;需要严格顺序用顺序消费

### 延伸追问(≥3)

1. **Disruptor 的容量为什么必须是 2 的幂?**
- 要点:用 `index & (size-1)` 位运算代替 `%` 取模,比除法快 10-100 倍
- 追问落点:JVM 层面 JIT 是否会优化 `%` 到 `&`?(不会主动优化)

2. **什么是伪共享?如何避免?**
- 要点:不同线程修改同一 cache line 内的相邻字段导致频繁 cache 失效;用 padding 保证字段独占 cache line
- 追问落点:JDK 9+ `@sun.misc.Contended` 注解

3. **Disruptor 的多消费者怎么避免读到过期数据?**
- 要点:SequenceBarrier 依赖其他消费者的位置,取所有消费者的最小值作为可消费起点
- 追问落点:与"读多写少无锁读"的对比

4. **Disruptor 和 Kafka / Redis 有什么区别?**
- 要点:Disruptor 是进程内低延迟队列,Kafka/Redis 是分布式/网络级;Disruptor 单机内存队列
- 追问落点:什么场景需要引入 Disruptor 而不是直接 Kafka

5. **Disruptor 元素预分配有什么代价?**
- 要点:启动时占用固定内存,不能动态扩容;事件对象要可复用(`onEvent` 后清空)
- 追问落点:如何设计可复用的 Event 类

### 面试者参考回答(500–800 字)

**【结论】** Disruptor 的低延迟靠**五件套**:环形数组 + 2 的幂 + 缓存行填充 + 元素预分配 + 批处理;用 SequenceBarrier 协调多消费者依赖。

**【三点】**
第一,**基础结构**:环形数组 + 序列号。生产者推进 cursor,多消费者各维护自己的 sequence;用 CAS 更新 cursor 无锁协调。空判定 `cursor == slowestConsumer`,满判定 `cursor - slowest == size`。

第二,**低延迟灵魂(5 个关键点)**:
- **容量取 2 的幂**:`index & (size-1)` 位运算代替取模,比除法快 10-100 倍
- **缓存行 padding**:序号字段 padding 到 64 字节独占 cache line,消除伪共享——多个线程频繁修改同一 cache line 会导致跨核 cache 频繁失效,性能损失巨大
- **元素预分配**:启动时全部创建 EventObject,运行期不 new,零 GC 压力
- **SequenceBarrier 依赖等待**:消费者按依赖判断可消费位置,避免忙等
- **批处理**:一次取连续区间 [seqFrom, seqTo],减少 CAS 和等待次数

第三,**与其他队列的对比**:ArrayBlockingQueue 用 ReentrantLock 高竞争慢;ConcurrentLinkedQueue 无锁但节点非预分配有 GC;Disruptor 在高频行情、日志流处理等场景能到亚微秒级延迟。

**【边界】** 两点提醒:一是 Disruptor 适合**依赖简单、消息固定长度**的场景;复杂业务、变长消息、需要持久化的场景直接用 Kafka/Redis 更合适。二是 Disruptor 学习成本高、灵活性低,普通业务解耦用 BlockingQueue 就够了。

---

📚 查看全部面试记录