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

2026-10-26

Java 并发深度 —— JMM/活锁死锁/线程池/无锁化/顺序执行/虚拟线程/虚拟线程×sync/synchronized 三特性/synchronized 竞争/Disruptor

10 题 · 平均 2.9/5 · low 3 · medium 2.75 · high 3 · 第 28 轮

#1 什么是Java内存模型(JMM)? low java-concurrency

题目

什么是Java内存模型(JMM)?

我的回答

java 内存模型 jmm 是 java 参考 mesi 内存模型而创建的一套自己管理对象,处理并发的一套规则;其核心关键字常用的就两个 volatile 和 synchronized,主要处理多线程下原子性、可见性、有序性问题;通过 synchronized 来实现锁,保证原子性、可见性、有序性,内核是使用 cpu 的 lock 指令,来保证原子性;通过 volatile 的刷新 cpu 主存机制实现可见性;通过 happend-before 规则中定义的逻辑来实现有序性。

点评

优点:- 三大问题(原子性 / 可见性 / 有序性)**术语正确**——比第 1 次(2026-09-29,误说"一致性")明显进步 - 提到 lock 指令实现原子性——**方向对**(CPU 层的 `cmpxchg + lock` 前缀) - 提到 happens-before 规则——**方向对**
不足:- **术语错**:说"参考 MESI 内存模型创建"——JMM 是 JLS 定义的**抽象规范**,与 MESI 互补不替代;MESI 是硬件缓存一致性协议,JMM 是软件规范 - **机制描述过粗**:volatile 不是"刷新 CPU 主存"这么简单,是**内存屏障(StoreStore/StoreLoad/LoadLoad/LoadStore)** 组合;synchronized 的原子性也不是纯 lock 指令,是 Monitor owner + CAS + entryList - **完全漏了三层结构**(主内存 / 工作内存 / 寄存器缓存)——这是 JMM 的核心抽象 - 未提 JMM ≠ JVM 的边界 - 未提 volatile 不保证原子性(`i++` 仍不安全)

完整讲解

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

### L1 · 30 秒速记

> **结论**:JMM = JLS 定义的**抽象规范**(非 JVM 实现),核心是主内存/工作内存三层结构 + 内存屏障 + happens-before。
> **关键词**:主内存 vs 工作内存 · 内存屏障(StoreLoad 等) · happens-before 7 规则
> **边界**:JMM ≠ JVM;MESI 是硬件层,JMM 是软件规范,互补不替代

### L2 · 核心讲解

**核心考点定位**:能否理解 JMM 是 JLS 定义的抽象规范,能否讲清三层结构、三大问题、两大关键字的作用。

**机制链(3 环)**:
1. 硬件差异(x86/ARM)+ 编译器优化(指令重排)+ CPU 缓存 → Java 代码在不同平台上并发语义不可预测
2. JMM 作为 JLS 抽象规范,屏蔽底层差异 → 定义主内存/工作内存抽象,规定线程如何交换变量值
3. 三大问题(原子性/可见性/有序性)→ volatile(内存屏障)、synchronized(monitor + happens-before)、AtomicXxx(CAS)分别解决

**证据锚点**(去重 ≥6):
- JMM = Java Language Specification 定义,非 JVM
- 主内存(Main Memory):所有线程共享,对应物理内存
- 工作内存(Working Memory):线程私有,对应 CPU 缓存 + 寄存器
- volatile 关键词:可见性 + 有序性(内存屏障)
- synchronized 关键词:原子性 + 可见性 + 有序性(monitor 机制 + happens-before)
- happens-before 7 规则:程序顺序、监视器锁、volatile 变量、线程启动、线程终止、线程中断、对象终结
- 内存屏障:StoreStore / StoreLoad / LoadLoad / LoadStore
- MESI 协议:硬件层缓存一致性(Modified/Exclusive/Shared/Invalid)

**常见误区**:
1. 错误:"JMM 就是 JVM" → 正确:JMM 是 JLS 定义的抽象规范,JVM 是具体实现(HotSpot/OpenJ9) → 为什么:规范与实现分离,便于跨平台兼容
2. 错误:"JMM 参考 MESI 内存模型创建" → 正确:MESI 是硬件缓存一致性协议,JMM 是软件规范,两者互补不替代 → 为什么:层次不同,MESI 解决多核缓存同步,JMM 解决线程间变量可见性
3. 错误:"JMM 三大问题是原子性/可见性/一致性" → 正确:三大问题是原子性/可见性/有序性("一致性"是 CAP 事务概念) → 为什么:JMM 用语序性(ordering),不用一致性(consistency)
4. 错误:"volatile 保证原子性" → 正确:volatile 只保可见性 + 有序性,不保原子性(`i++` 三步仍会交错) → 为什么:volatile 语义边界是"单变量一次读写"
5. 错误:"JMM 就是 Java 的内存模型" → 正确:JMM 不管理内存,只规定并发语义 → 为什么:内存管理是 GC / 堆栈分配,与 JMM 无关

**对比与权衡**:

| 概念 | 层次 | 作用 | 与其他的关系 |
|---|---|---|---|
| JMM | JLS 规范 | 并发语义抽象 | 屏蔽 JVM 差异 |
| JVM | 实现 | 具体执行(HotSpot/OpenJ9) | 实现 JMM 规范 |
| MESI | 硬件协议 | 多核缓存一致性 | 硬件层,与 JMM 互补 |
| 内存屏障 | 指令层 | 禁止指令重排 | JMM 的实现手段 |
| happens-before | 抽象规则 | 可见性契约 | JMM 提供的契约 |

**权衡**:JMM 是"规范 vs 实现"的分层设计,好处是跨平台兼容,代价是开发者需要理解规范而非依赖某个 JVM 的行为。

**适用边界**:
- **适合**:写并发代码时选择同步手段;理解为什么需要 volatile / synchronized
- **不适合**:性能调优(那是 JVM 参数、GC 调优的领域);硬件层问题(那是 MESI 层)
- **替代方案**:原子操作(AtomicXxx)用 CAS 解决原子性;无共享状态(不可变/ThreadLocal)避开同步

### 延伸追问(≥3)

1. **JMM 和 JVM 有什么本质区别?**
- 要点:JMM 是 JLS 定义的规范,JVM 是具体实现(HotSpot、OpenJ9)
- 追问落点:多 JVM 实现下 JMM 兼容的意义

2. **为什么有了 MESI 协议还需要 JMM?**
- 要点:MESI 解决缓存一致性(多核 CPU 间),JMM 解决指令重排 + 可见性抽象,两者互补
- 追问落点:硬件层 vs 软件层的边界

3. **volatile 不能保证原子性,为什么 double-check 还需要它?**
- 要点:volatile 提供内存屏障,禁止指令重排——防止"new 出对象但字段未初始化就被其他线程看到"这种半初始化状态
- 追问落点:DCL 单例的正确写法

4. **happens-before 有哪些规则?**
- 要点:7 条(程序顺序、监视器锁、volatile 变量、线程启动、线程终止、线程中断、对象终结)
- 追问落点:与 volatile / synchronized 的对应关系

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

**【结论】** JMM 是 JLS 定义的一套**抽象规范**,不是 JVM 本身;它屏蔽底层硬件差异,让 Java 代码在不同 CPU 上有可预测的并发语义。

**【三点】**
第一,**三层结构**:主内存(对应物理内存,所有线程共享)和工作内存(对应 CPU 缓存 + 寄存器,每个线程私有)。线程只操作工作内存里的变量副本,什么时候把值刷回主内存、什么时候从主内存读回来,由 JMM 规则决定。

第二,**三大问题 + 对应手段**:原子性(i++ 是否被拆)用 AtomicXxx / synchronized;可见性(A 改了 B 能否看到)用 volatile(内存屏障)/ synchronized;有序性(指令能否被重排)用 volatile(屏障)/ happens-before 规则。

第三,**易混点**:JMM ≠ JVM(规范 vs 实现);MESI 是硬件缓存一致性协议,JMM 是软件规范,互补而非替代;volatile 保证可见性和有序性但**不保证原子性**(`count++` 不能用 volatile);JMM 用"有序性"术语,"一致性"是 CAP/事务领域概念。

**【边界】** 两点提醒:一是 JMM 不管理内存,只规定并发语义(内存管理是 GC 和堆栈分配);二是 JMM 抽象规范不告诉你如何优化性能,那是 JVM 参数和 GC 调优的领域。

---

#2 什么是活锁,和死锁有什么区别? low java-concurrency

题目

什么是活锁,和死锁有什么区别?

我的回答

活锁也是线程争抢资源产生的一个问题,危害性也非常高。活锁是线程持有资源,和其他线程出现争抢后就释放资源,然后重新再去争抢,多线程相互争抢然后被人抢就释放,导致多线程一直无法完成自己的任务一直在释放、争抢、释放循环,线程是 RUNNABLE 不是 BLOCKED 会空耗 CPU 资源;死锁则是线程相互阻塞,卡住无法继续下去大家谁也不释放自己持有的资源互相等待,线程 BLOCKED。

点评

优点:- **Runnable vs BLOCKED 状态区分准确**——活锁空耗 CPU、死锁阻塞等待,两者状态机不同 - **指出"持有→释放→再抢→再释放"循环模式**,抓住活锁核心特征 - **点出危害(空耗 CPU 但不推进)**,这是死锁没有的性能代价
不足:- 漏了**活锁的解法**——随机退避(back-off with jitter)打破循环同步 - 未提**活锁与饥饿**的关系:活锁可能演化为饥饿(其他线程永远得不到资源) - 死锁的四条件(互斥/占有且等待/不可剥夺/循环等待)未展开

完整讲解

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

### L1 · 30 秒速记

> **结论**:活锁和死锁本质区别在**线程状态**——死锁 BLOCKED 卡住不动,活锁 RUNNABLE 空转但不推进。
> **关键词**:BLOCKED vs RUNNABLE · 状态循环 · 随机退避
> **边界**:三者都可能演化为饥饿,监控上靠线程状态分布 + CPU 使用区分

### L2 · 核心讲解

**核心考点定位**:能否准确区分活锁、死锁、饥饿三种并发故障,抓住各自的**状态机特征**和**危害方式**。

**机制链(3 环)**:
1. 多线程争抢资源 → 线程状态机分岔:一方"释放重抢"进入活循环 vs 一方"占着不放"进入阻塞 → **状态决定危害方式**
2. 活锁的"释放重抢"模式 → 双方状态同步退让,谁也无法独占资源 → **RUNNABLE 空耗 CPU 但业务零推进**
3. 死锁的"占着不放"模式 → 循环等待形成闭环,任一条件不满足即崩 → **BLOCKED 堆积,JVM 内置 `Found one Java-level deadlock` 检测**

**证据锚点**(去重 ≥6):
- `jstack`:抓线程栈,看 `java.lang.Thread.State: BLOCKED` / `RUNNABLE` 分布
- `Found one Java-level deadlock`:JVM 内置死锁检测关键字(jstack / jconsole 输出)
- 死锁四条件:互斥 · 占有且等待 · 不可剥夺 · 循环等待
- 活锁典型场景:两线程互相让步(窄走廊各向左向右)
- 饥饿触发条件:低优先级线程被高优先级长期抢占
- 排查命令:`jstack <pid>` 看 `Found one Java-level deadlock`;`top -Hp` 看 CPU 高的线程

**对比与权衡**:

| 故障类型 | 线程状态 | CPU 消耗 | 是否推进 | 检测手段 | 解决方式 |
|---|---|---|---|---|---|
| 死锁 | BLOCKED | 低(等待) | 否 | `jstack` 检测 `Found one Java-level deadlock` | kill / dump 栈分析后修复 |
| 活锁 | RUNNABLE | 高(空转) | 否 | 线程 RUNNABLE 堆积 + CPU 高 + 业务 QPS 不涨 | 随机退避(jitter / back-off)破坏循环 |
| 饥饿 | RUNNABLE/BLOCKED | 不定 | 否 | 长时间无业务指标变化 | 公平锁 / 优先级随机化 |

**常见误区**:
1. 错误:"活锁就是死锁的另一种说法" → 正确:两者状态不同(BLOCKED vs RUNNABLE),危害方式不同(等待 vs 空耗) → 为什么:混淆会让排查方向偏,活锁要靠 CPU 定位而不是等 jstack 报
2. 错误:"活锁一定会自动恢复" → 正确:活锁可能长期持续,且可能演化为饥饿(其他线程永远拿不到资源) → 为什么:循环对称性未被打破前不会自愈
3. 错误:"死锁靠 jstack 检测,活锁无解" → 正确:活锁可通过随机退避破坏循环对称性,让双方退让时间错开 → 为什么:只要打破循环同步,双方就能自然走出活循环

**适用边界**:
- **适合**:并发编程故障排查(BLOCKED 堆积 → 死锁;RUNNABLE 空耗 + 无推进 → 活锁);设计层面预防(选无锁 / 公平锁 / 随机退避)
- **不适合**:性能问题排查(活锁空耗 CPU 但性能瓶颈可能还有其他原因)
- **替代方案**:`ReentrantLock(true)` 公平锁避免饥饿;无锁化编程从根上避开三者

### 延伸追问(≥3)

1. **线上活锁怎么监控和识别?**
- 要点:RUNNABLE 线程堆积 + CPU 高 + 业务指标无推进(QPS 不涨、订单量不涨);对比死锁(BLOCKED 堆积 + jstack 检测)
- 追问落点:如何用 Prometheus + JFR 组合告警

2. **JVM 有内置的活锁检测吗?**
- 要点:JVM 只内置死锁检测(`Found one Java-level deadlock`);活锁没有内置检测,只能靠线程状态分布 + 业务指标侧写
- 追问落点:如何基于 `ThreadMXBean` 自定义活锁监控

3. **活锁怎么避免?**
- 要点:随机退避(back-off with jitter)打破循环同步;限制循环次数;选公平锁或无锁算法
- 追问落点:与饥饿的区别与共同解决方案

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

**【结论】** 活锁和死锁本质区别在线程状态——死锁 BLOCKED 卡住不动,活锁 RUNNABLE 空转但不推进。

**【三点】**
第一,**死锁**:多个线程互相持有对方需要的资源,都进入 BLOCKED 状态等待对方释放,谁也不动,不消耗 CPU 也不推进。典型触发是四条件同时满足(互斥、占有且等待、不可剥夺、循环等待),线上靠 jstack 抓栈定位,JVM 内置 `Found one Java-level deadlock` 检测。

第二,**活锁**:线程都在主动变化状态试图推进,但状态变化恰好让彼此都无法前进,形成活循环——比如两个线程都在让步给对方,各自向左再向右反复退让。线程状态是 RUNNABLE 而不是 BLOCKED,所以空耗 CPU 资源,但业务实际不推进。这是活锁和死锁最大的差异:死锁"卡住不动",活锁"忙而不进"。

第三,**监控识别与解法**:死锁看 BLOCKED 堆积 + jstack 检测;活锁看 RUNNABLE 堆积 + CPU 高 + 业务指标无推进(QPS 不涨)。解法上死锁只能被外部打破(kill / dump 栈分析后修复);活锁靠随机退避(back-off with jitter)破坏循环对称性,让双方退让时间错开自然退出;饥饿靠公平锁或优先级随机化。

**【边界】** 三者都可能演化为饥饿——死锁若被中断可能变活锁;活锁若循环对称性未打破长期存在可能演化为饥饿(其他线程永远拿不到资源)。所以设计上要一开始就选无锁或公平锁避免这些问题。

---

#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

题目

如何实现无锁化编程?

我的回答

无锁化编程方面有多种实现方式;首先可以规避共享对象的变更,没有多线程共享的资源也就没有锁的必要了;相关的有 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 隐蔽,链表复用等场景常见,必须用版本号防护。

#5 如何让Java的线程池顺序执行任务? medium java-concurrency

题目

如何让Java的线程池顺序执行任务?

我的回答

想要让 java 线程池顺序执行任务,可以考虑用 join、用锁、用唤醒策略等;多线程本身目的其实是异步执行,而不是顺序化执行任务,顺序执行直接同步就好了;但是非要顺序执行多线程的话可以让多线程之间有自己的通信,比如通过 wait 和唤醒命令一个线程完成再唤醒下一个线程,顺序唤醒;或者是通过 join 命令一个线程执行后再起下一个线程;或者通过 volatile 的可见性,控制多线程按自己线程状态值去执行,然后实现顺序执行任务。

点评

优点:- **认知到位**:"多线程目的是异步执行,非必要不串行"——这句话是加分表达,说明理解了并发编程的原始意图 - 列举了 wait/notify、join、volatile 状态位三种跨线程同步手段
不足:- **完全漏了最经典的答案**:`newFixedThreadPool(1)` / `newSingleThreadExecutor()`——把 corePoolSize 和 maximumPoolSize 都设为 1,天然顺序执行,这是面试必答项 - 未提**分片内串行、分片间并行**的分片模式(order by key) - 未提 `Future.get()` / `CompletableFuture.thenApply` 任务链编排(更工程化) - wait/notify 是低级手段,生产代码里几乎不直接用

完整讲解

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

### L1 · 30 秒速记

> **结论**:"顺序执行"本质是**串行化**——最简单答案是 `newSingleThreadExecutor()`;进阶是分片内串行(按 key hash 分片)。
> **关键词**:SingleThreadExecutor · hash 分片保序 · 任务链 CompletableFuture
> **边界**:多线程目的是异步,非必要不串行;wait/notify / join / volatile 状态位都是反模式

### L2 · 核心讲解

**核心考点定位**:能否识别"顺序执行"的本质是**串行化**,能否区分"完全串行"与"分片内串行"两种模式。

**机制链(3 环)**:
1. 业务需要"任务按顺序"→ 判断是完全串行还是分片内串行 → **完全串行 = 单线程池;分片内串行 = 按 key hash 分片**
2. 单线程池 → 内部用 `LinkedBlockingQueue` 无界队列 → **任务提交快于消费时会 OOM**,必须换有界 `ArrayBlockingQueue`
3. 分片内串行 → 同 key 任务落到同一线程,不同 key 并行 → **同 Kafka Partition 保序 / RocketMQ 顺序消费**,本质都是这个思路

**证据锚点**(去重 ≥10):
- `Executors.newSingleThreadExecutor()`:单线程池 API,返回 TreadPoolExecutor 可动态调整
- `newFixedThreadPool(1)`:功能等价但返回 final ExecutorService 不可动态调整
- `LinkedBlockingQueue`:默认无界,单线程池用这个容易 OOM
- `ArrayBlockingQueue(N)`:有界队列,配合拒绝策略防 OOM
- `hash(key) % N` 路由:分片内串行的核心公式
- Kafka Partition 保序:Consumer 单线程消费 Partition 内消息
- RocketMQ 顺序消费:MessageQueueSelector 按 ShardingKey 选 Queue
- CompletableFuture.thenApply:任务链编排(前一个完成才跑下一个)
- Future.get():阻塞等前一个完成
- wait/notify / join / volatile 状态位:**跨线程通信低级手段,生产不推荐**

**对比与权衡**:

| 方案 | 串行程度 | 吞吐 | 复杂度 | 适用场景 |
|---|---|---|---|---|
| SingleThreadExecutor | 完全串行 | 低(单线程) | 低 | 简单任务流 |
| hash 分片 + ExecutorService[] | 分片内串行、分片间并行 | 高 | 中 | 订单系统保序(按 user_id 分片) |
| CompletableFuture.thenApply 链 | 任务链串行 | 中 | 中 | 依赖关系明确的任务 |
| wait/notify + 状态位 | 完全串行 | 低 | 高(易死锁) | ❌ 反模式 |

**常见误区**:
1. 错误:"多线程下顺序执行只能用 join" → 正确:join 是线程级同步低级手段,`newSingleThreadExecutor()` 才是最简单的答案 → 为什么:join 需要跨线程调用,写起来复杂还容易死锁
2. 错误:"SingleThreadExecutor 和 newFixedThreadPool(1) 一样" → 正确:前者返回 TreadPoolExecutor 可动态调整参数,后者返回 final ExecutorService → 为什么:动态调参能力是生产刚需
3. 错误:"顺序执行就是所有任务排队" → 正确:分片内串行是更好的方案——按 key hash 分片,同 key 保序不同 key 并行,Kafka Partition 就是这个思路 → 为什么:完全串行吞吐受限,业务侧往往只需要同 key 保序

**适用边界**:
- **适合**:需要"同一业务实体内保序"(如同一订单状态流转);用 hash 分片方案
- **不适合**:任务依赖关系明确但 key 不同的场景(用 CompletableFuture 链更清晰);多线程目的就是异步,若真需要同步还不如直接改回单线程
- **反模式**:用 wait/notify / join / volatile 状态位串线程——跨线程通信低级手段,生产代码不推荐

### 延伸追问(≥3)

1. **SingleThreadExecutor 和 newFixedThreadPool(1) 有什么区别?**
- 要点:前者返回 TreadPoolExecutor 可动态调整参数,后者返回 final ExecutorService 不可动态调整
- 追问落点:生产环境为什么需要动态调参能力

2. **业务侧如何按 key 分片保序?**
- 要点:`hash(key) % N` 路由到 `ExecutorService[]`;N 通常取 2 的幂或 CPU 核数
- 追问落点:与 Kafka Partition、RocketMQ Queue 的对比

3. **MQ 的顺序消费和线程池的顺序执行有什么关联?**
- 要点:本质都是"分片内串行"——Kafka Partition 内有序 + Consumer 单线程消费 = 顺序消息
- 追问落点:MQ 顺序消费为什么扩容消费者无效(Queue 数 < 消费者数时)

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

**【结论】** "顺序执行"本质是**串行化**——最简单是 `newSingleThreadExecutor()`;进阶是分片内串行(按 key hash 分片),Kafka Partition 保序也是这个思路。

**【三点】**
第一,**完全串行**:直接用 `Executors.newSingleThreadExecutor()` 或 `newFixedThreadPool(1)`,一个线程顺序处理所有任务。但有个坑——内部用无界 `LinkedBlockingQueue`,任务提交快于消费时会 OOM,生产要用有界 `ArrayBlockingQueue`。

第二,**分片内串行、分片间并行**(项目里更常用):如果订单 A/B/C 是不同业务的,它们不需要互相等待,只需要"同一订单内的操作顺序执行"就够了。做法是按业务键 hash 分片——`hash(user_id) % N` 路由到 `ExecutorService[]` 数组,每个分片一个线程。同一个 user 的任务一定落到同一线程保序,不同 user 并行不互相影响。Kafka 的 Partition 保序、RocketMQ 的顺序消费本质都是这个思路。

第三,**任务链编排**:如果是"任务 A 完成后再跑任务 B"这种依赖关系,用 `Future.get()` 阻塞等前一个完成,或者用 `CompletableFuture.thenApply` 声明式串联,都比 wait/notify 或 join 更工程化。

**【边界】** 至于 wait/notify、join、volatile 状态位——这些都是**跨线程通信的低级手段**,生产代码里几乎不直接用,容易写出死锁或状态错乱。多线程的本意是异步执行,如果一定要串行,那还不如考虑单线程池或把业务改回同步,减少不必要的复杂度。

---

#6 JDK21中的虚拟线程是怎么回事? medium java-concurrency

题目

JDK21中的虚拟线程是怎么回事?

我的回答

jdk21 的虚拟线程是一种简化线程的资源实现,不像线程是操作系统管控的,虚拟线程是由 jvm 管控的轻量级线程;执行的时候,并不像是线程和操作系统线程一对一,而是大量的虚拟线程对应少量的操作系统线程去执行任务。

点评

优点:- **JVM 管控的轻量级线程**——理解到位 - **M:N 映射(大量虚拟线程对应少量系统线程)**——核心概念抓对了 - 与操作系统线程做了区分
不足:- **完全漏了底层机制**:虚拟线程通过 **Continuation**(可挂起的调用栈)实现;阻塞时 carrier thread 切换出去,虚拟线程的栈被栈化保存 - 未提**使用边界**:IO 密集用(阻塞多),CPU 密集不用(无收益) - 未提 **pinning** 问题(synchronized / JNI 阻塞时 carrier 被固定) - 未提**与 Go goroutine 的对比**(都是 M:N 但实现不同) - 未提**栈是可变大小的**,每个虚拟线程初始只占几 KB

完整讲解

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

### L1 · 30 秒速记

> **结论**:虚拟线程 = JVM 层 M:N 调度,靠 **Continuation**(可挂起栈)实现,IO 密集下能开百万级并发。
> **关键词**:Continuation · M:N 调度 · pinning 陷阱 · IO 密集适用
> **边界**:CPU 密集无收益;synchronized 块内阻塞、JNI 会 pin carrier thread

### L2 · 核心讲解

**核心考点定位**:能否说清虚拟线程的**底层机制(Continuation)**、**M:N 调度模型**、**适用场景与 pinning 陷阱**。

**机制链(3 环)**:
1. 传统线程 = 1:1 映射 OS 线程,创建成本 ~1MB 栈 → 大规模并发受限于 OS 线程数上限(数万级)
2. 虚拟线程通过 **Continuation** 实现挂起:阻塞时把整个栈帧保存到堆内存,释放 carrier thread 干别的 → 唤醒时把栈帧还原到另一个 carrier thread 继续执行
3. M:N 调度:大量虚拟线程(百万级)共享少量 OS 线程(carrier threads,默认 CPU 核数)→ JVM ForkJoinPool 负责调度;栈是可变大小的(32-512B 起,最大 1MB)

**证据锚点**(去重 ≥10):
- JDK 21:虚拟线程 GA(JEP 444)
- `Thread.ofVirtual().start()`:创建虚拟线程 API
- `Executors.newVirtualThreadPerTaskExecutor()`:每任务一个虚拟线程
- Continuation:可挂起的调用栈,虚拟线程核心抽象
- Carrier thread:OS 线程,默认 CPU 核数
- 栈大小:初始 32-512B,按需扩容,最大 1MB(`-Djdk.virtualThreadScheduler.stackSize`)
- `-Djdk.tracePinnedThreads=full`:排查 pinning 参数
- JFR `jdk.VirtualThreadPinned` 事件:监控 pinning
- Spring Boot 3.2+:官方支持虚拟线程
- HikariCP 默认 max=CPU*10:虚拟线程模型下需拉高

**对比与权衡**:

| 维度 | OS 线程 | 虚拟线程 |
|---|---|---|
| 映射模型 | 1:1 | M:N |
| 栈大小 | 固定 1MB | 可变 32B-1MB |
| 并发量级 | 数万 | 百万级 |
| 调度者 | 操作系统 | JVM ForkJoinPool |
| 创建成本 | 高(系统调用) | 低(用户态) |
| 阻塞代价 | 独占 OS 线程 | 挂起后 carrier 释放 |
| pinning | 无 | 存在(synchronized 阻塞 / JNI) |
| 适用 | CPU 密集 | IO 密集 |

**常见误区**:
1. 错误:"虚拟线程可以无限开" → 正确:百万级是量级参考,但每个栈仍需内存;`-Djdk.virtualThreadScheduler.stackSize` 可限制上限 → 为什么:栈是可变大小但仍有上限
2. 错误:"虚拟线程能替代所有线程池" → 正确:CPU 密集场景无收益(还是 CPU 核数决定吞吐),IO 密集才适用 → 为什么:虚拟线程优势在阻塞时释放 carrier,CPU 密集不阻塞
3. 错误:"synchronized 在虚拟线程下完全不能用" → 正确:纯计算不 pin,只是别在 synchronized 块里阻塞 → 为什么:pin 发生在阻塞时,不是持有锁时
4. 错误:"虚拟线程栈和 OS 线程栈一样大" → 正确:虚拟线程栈可变(32B-1MB),OS 线程固定 1MB → 为什么:可变栈是百万级并发的关键

**适用边界**:
- **适合**:IO 密集(HTTP 调用、DB 查询、RPC、文件读写);阻塞多的场景(sleep、wait);一个请求一个虚拟线程模型
- **不适合**:CPU 密集(无收益);synchronized 关键路径(pin 风险)
- **配套改造**:连接池(HikariCP)默认 max 需拉高;监控(JFR 看 pinning)

### 延伸追问(≥3)

1. **虚拟线程和 Go goroutine 有什么本质区别?**
- 要点:goroutine 栈更小(2KB 起)、有 GC;虚拟线程栈可变(32B 起)、无独立 GC;goroutine 有 GOMAXPROCS 调度参数,虚拟线程默认 CPU 核数
- 追问落点:两者的 GMP vs ForkJoinPool 调度模型差异

2. **虚拟线程下 Spring 事务、JPA 需要改造吗?**
- 要点:Spring Boot 3.2+ 官方支持;HikariCP 默认 max=CPU*10 需拉高到几千才能利用虚拟线程优势
- 追问落点:Tomcat 10.1+ 的虚拟线程集成方式

3. **怎么排查 pinning?**
- 要点:`-Djdk.tracePinnedThreads=full` JVM 参数打印 pin 调用栈;JFR `jdk.VirtualThreadPinned` 事件;carrier thread 利用率低说明有 pin
- 追问落点:JDK 24 的 experimental 参数 `-Djdk.virtualThreadScheduler.mountPinningPolicy` 尝试缓解

4. **虚拟线程会替代线程池吗?**
- 要点:不会——CPU 密集场景线程池仍必要;虚拟线程更适合作为 Servlet 容器的执行层,线程池仍用于 CPU 密集任务
- 追问落点:生产环境如何组合使用

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

**【结论】** JDK 21 虚拟线程本质是 JVM 层 M:N 调度模型——大量虚拟线程共享少量 OS 线程(carrier threads),靠 Continuation 实现挂起。

**【三点】**
第一,**底层机制**:虚拟线程通过 **Continuation** 实现——可挂起的调用栈。阻塞时把栈帧保存到堆内存,释放 carrier thread 执行其他虚拟线程;唤醒时把栈帧还原到另一个 carrier thread 继续。栈是可变大小(32-512B 起,最大 1MB),所以能开百万级虚拟线程。

第二,**关键陷阱是 pinning**:虚拟线程在 `synchronized` 块里阻塞(IO、sleep、join)或调用 native 方法(JNI)时,carrier thread 会被**固定**在这个虚拟线程上无法卸载,导致 M:N 退化。JDK 21 用 `ReentrantLock` 缓解——uncontained 时不 pin。排查靠 `-Djdk.tracePinnedThreads=full` 或 JFR `jdk.VirtualThreadPinned` 事件。

第三,**使用边界**:**适合** IO 密集(HTTP、DB、RPC),一个请求一个虚拟线程百万并发轻松扛;**不适合** CPU 密集(无收益);连接池要跟着调,HikariCP 默认 max=CPU*10 需拉高到几千。API 直接用 `Thread.ofVirtual().start()` 或 `Executors.newVirtualThreadPerTaskExecutor()`,Spring Boot 3.2+ 有官方支持。

**【边界】** 注意两点:一是虚拟线程不是万能的,CPU 密集场景线程池仍是最佳选择;二是生产部署要监控 pinning 事件,避免"看似用了虚拟线程,实际 carrier 被固定"的退化。

---

#7 为什么虚拟线程不能用synchronized? medium java-concurrency

题目

为什么虚拟线程不能用synchronized?

我的回答

虚拟线程不用 synchronized 的核心原因是 synchronized 是操作系统层面的锁,会直接将虚拟线程对应的系统线程锁住,这样最后会导致虚拟线程退化成常规线程,和系统线程一对一,会影响虚拟线程使用,可以用 java 的 ReentrantLock 来做锁操作。

点评

优点:- **主因(carrier thread 被固定)方向对**——理解到 pinning 的核心机制 - **提到 ReentrantLock 替代方案**——工程实践中确实常用 - 认知到"退化到 1:1"的性能代价
不足:- **表述过度简化**:说"退化成常规线程,和系统线程一对一"不准确——实际是 **carrier thread 被 pin,无法卸载其他虚拟线程**,不是"整个虚拟线程模型退化为 OS 线程" - **过度否定 synchronized**:JDK 21 官方立场是"synchronized 尽量用,但不能在其中阻塞"——只在 synchronized 块里 IO 阻塞或 sleep 才 pin,纯计算不 pin - ReentrantLock **竞争时也会 pin**,只是 uncontended 时不 pin——这个边界没讲清 - 未提**JNI / native 方法也会 pin** - 未提排查手段(`-Djdk.tracePinnedThreads` / JFR `jdk.VirtualThreadPinned`)

完整讲解

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

### L1 · 30 秒速记

> **结论**:不是"synchronized 不能用",而是"别在 synchronized 块里阻塞"——阻塞时 carrier thread 被 pin。
> **关键词**:pinning · Monitor owner 与 OS 线程绑定 · ReentrantLock uncontended 不 pin
> **边界**:ReentrantLock 竞争时也 pin;彻底避免靠 CAS/无锁;JNI 也会 pin

### L2 · 核心讲解

**核心考点定位**:能否准确理解 **pinning** 的机制(不是"退化为 OS 线程",而是 carrier thread 被固定),能否给出**边界清晰的替代方案**。

**机制链(3 环)**:
1. 虚拟线程靠 Continuation 挂起 → 阻塞时把栈帧保存到堆内存,释放 carrier thread
2. `synchronized` 持锁期间阻塞 → Monitor owner 关联在 OS 线程上,栈帧无法安全挂起 → carrier thread 陪着等待 → **这就是 pinning**
3. 后果:M:N 调度退化 → carrier thread 无法卸载到其他虚拟线程 → 虚拟线程并发优势消失

**证据锚点**(去重 ≥10):
- Monitor owner:synchronized 的锁所有者,绑定 OS 线程
- Continuation:虚拟线程的挂起栈,需要与 Monitor owner 解耦才能安全挂起
- Carrier thread:OS 线程,虚拟线程的载体
- `-Djdk.tracePinnedThreads=full`:JVM 参数,打印 pin 调用栈
- JFR `jdk.VirtualThreadPinned` 事件:监控 pinning
- ReentrantLock.tryLock(timeout):超时放弃不 pin
- AtomicReference / CAS:无锁化彻底避免 pin
- JDK 24 experimental:`-Djdk.virtualThreadScheduler.mountPinningPolicy` 尝试缓解
- `Thread.onSpinWait()`:某些场景也会 pin
- Native 方法(JNI):调用时 carrier 也被固定

**对比与权衡**:

| 锁类型 | uncontended 阻塞 | 竞争时阻塞 | 其他阻塞 | pin 程度 |
|---|---|---|---|---|
| `synchronized` | pin | pin | pin | 高 |
| `ReentrantLock` | 不 pin | pin | 不 pin | 中 |
| `tryLock(timeout)` | 不 pin | 不 pin(超时放弃) | 不 pin | 低 |
| CAS / AtomicReference | 不 pin | 不 pin | 不 pin | 无 |
| JNI native | — | — | pin | 高 |

**常见误区**:
1. 错误:"synchronized 在虚拟线程下完全不能用" → 正确:JDK 21 官方立场是"尽量用,只是别在其中阻塞",纯计算不 pin → 为什么:pin 发生在阻塞时,不是持有锁时
2. 错误:"synchronized 会让虚拟线程退化成 OS 线程 1:1" → 正确:不是退化到 1:1,而是 carrier thread 被固定无法卸载其他虚拟线程 → 为什么:M:N 退化的准确描述是"carrier 利用率下降"
3. 错误:"ReentrantLock 完全不会 pin" → 正确:uncontained 时不 pin,真竞争(park)时也 pin → 为什么:ReentrantLock 只是概率更低,不是零
4. 错误:"虚拟线程的 pinning 只跟 synchronized 有关" → 正确:JNI / native 方法也会 pin,与锁无关 → 为什么:native 调用时 JVM 无法调度虚拟线程
5. 错误:"只要用 ReentrantLock 就万事大吉" → 正确:高竞争场景 ReentrantLock 也会 pin,彻底解决要用 CAS / 无锁 → 为什么:并发原语的选择要看竞争程度

**适用边界**:
- **适合**:`synchronized` 块内纯计算(不阻塞);用 `ReentrantLock` 缓解;高竞争用 CAS / 无锁
- **不适合**:`synchronized` 块内 IO 阻塞、sleep、`join`;JNI 密集场景
- **替代方案**:`ReentrantLock` uncontended 无 pin;`tryLock` 超时放弃;CAS / AtomicReference 彻底无锁

### 延伸追问(≥3)

1. **ReentrantLock 竞争时也会 pin,那怎么完全避免 pin?**
- 要点:用无锁算法(CAS + AtomicReference)或 `tryLock` 非阻塞模式(超时直接放弃,不 pin)
- 追问落点:ConcurrentHashMap 的 CAS 无锁化设计

2. **JDK 22/24 是否修复了 pinning?**
- 要点:JDK 24 有 experimental 参数 `-Djdk.virtualThreadScheduler.mountPinningPolicy` 尝试缓解;JDK 22 主要是改进调度器
- 追问落点:JVM 层面 pinning 缓解的路线

3. **生产环境怎么判断有没有 pinning 问题?**
- 要点:JFR `jdk.VirtualThreadPinned` 事件;carrier thread 利用率低说明有 pin;`-Djdk.tracePinnedThreads` 打印栈
- 追问落点:如何在 Prometheus + JFR 上建立 pinning 告警

4. **虚拟线程下 synchronized 到底能不能用?**
- 要点:可以用,但只用于纯计算;阻塞必须用 ReentrantLock 或 CAS;生产环境建议监控 pinning
- 追问落点:Spring 生态对虚拟线程的适配(Boot 3.2+)

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

**【结论】** 不是"synchronized 不能用",而是"别在 synchronized 块里阻塞"——阻塞时 carrier thread 被 pin 无法卸载。

**【三点】**
第一,**为什么 pin**:虚拟线程靠 Continuation 挂起——阻塞时把栈帧保存到堆内存,释放 carrier thread 干别的。但 `synchronized` 块持锁期间如果阻塞(IO、sleep、join),JVM 不能挂起这个栈帧,因为锁的 Monitor owner 关联在 OS 线程上,一旦切换走再回来可能破坏锁的一致性。所以 carrier thread 只能陪着这个虚拟线程一起等——这就是 pinning。

第二,**准确的说法**:不是"synchronized 不能用",而是"别在 synchronized 块里阻塞"。如果 synchronized 块内是纯计算、快速方法,其实不 pin,JDK 21 官方立场也是"尽量用,只是别在其中阻塞"。

第三,**替代方案**:首选 `ReentrantLock`——uncontained 时完全不 pin,只有真正竞争(park)时才可能 pin,pin 概率远低于 synchronized。更彻底用 CAS + AtomicReference 无锁化,或 `tryLock` 非阻塞模式(超时直接放弃,不 pin)。注意:JNI / native 方法也会 pin,与锁无关。排查靠 `-Djdk.tracePinnedThreads=full` 或 JFR 的 `jdk.VirtualThreadPinned` 事件。

**【边界】** 两点提醒:一是 ReentrantLock 也不是零 pin,高竞争下还是会有;二是"完全无锁"要付出正确性代价,生产要根据业务权衡选择——语义正确性永远优先于 pinning 规避。

---

#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 就够了。

---

📚 查看全部面试记录