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

2026-09-29

Java 并发编程深度 —— 线程基础 → 锁 → JUC/AQS → JMM/volatile

10 题 · 平均 2.67/5 · low 2.6 · medium 2.75 · 第 1 轮

#1 什么是守护线程,和普通线程有什么区别? low java-concurrency

题目

什么是守护线程,和普通线程有什么区别?

我的回答

暂无回答记录

点评

评分:⭐☆☆☆☆(1/5)
核心问题:概念完全错位——把守护线程理解成"低优先级 + 在其他线程之后运行",实际核心是"是否让 JVM 拒绝退出"
必背:- 唯一区别在 JVM 退出条件:JVM 检查是否还有用户线程,用户线程全结束才退出,守护线程被强制终止 - 典型用途:GC / JIT / Finalizer / Reference Handler - 陷阱:`setDaemon(true)` 必须在 `start()` 之前调用

完整讲解

第一部分:知识重点剖析

**核心考点定位**:区分 Java 中"用户线程"和"守护线程"的生命周期语义,考察对 JVM 退出条件的理解。

**知识点拆解**:
1. **底层原理**:`Thread` 有一个 `daemon` 标志位(`setDaemon(true)` 设置),JVM 退出判断时**只看用户线程数**,守护线程不参与计数
2. **设计权衡**:守护线程没有"善后机会"(无 finally 保证执行),因此只适合"服务型"任务,不适合"必须完成"的业务
3. **工程实践**:GC、JIT、Finalizer、Reference Handler 等 JVM 内部线程都是守护线程;应用层的日志 flush、心跳上报通常也设为守护

第二部分:考察方向与延伸递进

**考察方向**:
- **深度**:JVM 退出条件的判断逻辑(`Thread.shouldYield()` / `java.lang.ApplicationShutdownHooks`)
- **广度**:守护线程 vs 优先级 vs 亲和性(`Thread.setPriority` / `Thread.setAffinityNanos`)
- **应用**:Spring Boot 优雅停机时守护线程的行为差异

**延伸递进面试题**:
1. **守护线程里的 finally 一定会执行吗?**
- 解答要点:不一定。JVM 退出时守护线程可能被强制终止,finally 无保证
2. **如何在守护线程里做收尾工作?**
- 解答要点:改用 `Runtime.addShutdownHook` 注册 hook;或改用用户线程 + `CountDownLatch`
3. **守护线程可以创建子线程吗?子线程的属性是什么?**
- 解答要点:可以。子线程继承父线程的 daemon 属性,除非显式改为 false

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

面试官您好,Java 里的线程分用户线程和守护线程两类,通过 `thread.setDaemon(true)` 把线程标记成守护。它们的唯一区别在于**JVM 的退出条件**:JVM 会检查系统里是否还有"用户线程"存活,只要有一个用户线程在跑,JVM 就不会退出;当所有用户线程都结束后,JVM 才会退出,此时守护线程会被强制终止,**没有善后机会**,也就是说守护线程里的 finally、关闭流这些"必须完成"的动作可能来不及执行。

守护线程的典型用途是"服务型的后台任务",比如 JVM 内部的 GC 线程、JIT 编译线程、Finalizer 线程、Reference Handler 线程,以及应用里的日志定期 flush、心跳上报这类任务——它们不需要保证执行完毕,服务完就行。

工程上的两个坑:第一,如果线程已经 `start()` 了再调用 `setDaemon` 会抛 `IllegalThreadStateException`,所以必须在 start 前设置;第二,守护线程创建的子线程默认继承守护属性,如果你希望子线程是用户线程,要显式 `setDaemon(false)`。

如果要让守护线程也能做收尾,可以用 `Runtime.addShutdownHook` 挂一个 hook,JVM 退出前会主动调 hook 里的代码,这就绕过了守护线程被强杀的坑。

#2 线程有几种状态,状态之间的流转是怎样的? medium java-concurrency

题目

线程有几种状态,状态之间的流转是怎样的?

我的回答

暂无回答记录

点评

评分:⭐⭐☆☆☆(2/5)
核心问题:多编了个"READY 状态"(Java 没有),漏了 TIMED_WAITING
必背:6 状态 = NEW / RUNNABLE / BLOCKED / WAITING / TIMED_WAITING / TERMINATED - BLOCKED 抢锁失败自动进入;WAITING 显式 wait/join/park;TIMED_WAITING 带超时的 wait/sleep/join - 中断不是 State 枚举,是独立标志位

完整讲解

第一部分:知识重点剖析

**核心考点定位**:考察对 `Thread.State` 枚举的完整掌握,以及状态间转移触发的条件——尤其是"等待类"三种状态的区别。

**知识点拆解**:
1. **底层原理**:`Thread.State` 是 JDK 定义的 6 值枚举,反映 JVM 层对线程生命周期的高度抽象——刻意省略了 OS 层的 READY 状态,因为"就绪+运行"对 Java 开发者来说没有可观察差异
2. **设计权衡**:把 BLOCKED、WAITING、TIMED_WAITING 分成三种,是因为它们的**唤醒机制**完全不同——BLOCKED 靠抢锁自动恢复,WAITING 靠 notify/unpark,TIMED_WAITING 会自己超时
3. **工程实践**:`jstack` 打印线程栈时每个线程都会显示当前 State,排查死锁/线程泄漏时是核心线索

第二部分:考察方向与延伸递进

**考察方向**:
- **深度**:BLOCKED vs WAITING 的字节码差异(`monitorenter` 失败 vs `wait()`/`park()`)
- **广度**:与操作系统线程状态(Running/Ready/Blocked/Sleeping)的对照
- **应用**:用 `ThreadMXBean` 或 `jstack` 打印线程状态排查线上问题

**延伸递进面试题**:
1. **BLOCKED 和 WAITING 的进入条件分别是什么?**
- 解答要点:BLOCKED 是抢 synchronized 锁失败自动进入;WAITING 是显式调用 wait/join/park
2. **为什么 WAITING 醒来后不是直接 Running,而是 RUNNABLE?**
- 解答要点:因为 Java 层没有 READY 概念,`getState()` 只会返回 RUNNABLE,实际是否持有 CPU 由 OS 调度决定
3. **中断后线程状态会变吗?**
- 解答要点:不会。中断只是设置 `interrupted` 标志位,State 枚举里没 INTERRUPTED;但 `sleep/wait/join` 被中断时会抛 `InterruptedException`,状态从 TIMED_WAITING/WAITING 回到 RUNNABLE

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

面试官您好,Java 里 `Thread.State` 一共 6 种:NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINATED。

流转是这样的:线程 `new` 出来是 NEW;调用 `start()` 进入 RUNNABLE;如果在 RUNNABLE 里去抢 synchronized 锁失败了,就进入 BLOCKED,抢到锁再回到 RUNNABLE;如果主动调 `wait()`、`join()` 或 `LockSupport.park()` 就进入 WAITING,被 notify/unpark 后回到 RUNNABLE;如果是带超时的 `sleep(n)`、`wait(n)`、`join(n)` 就进入 TIMED_WAITING,超时或被 notify 后回到 RUNNABLE;最后 run() 结束或抛异常进入 TERMINATED。

有两个坑要特别注意:第一,Java 没有 READY 状态,OS 层的"就绪+运行"在 JVM 层都归为 RUNNABLE,所以唤醒后 `getState()` 直接返回 RUNNABLE,不是先"回到准备"再"进运行";第二,中断不是状态,`isInterrupted()` 是独立标志位,State 枚举里没有 INTERRUPTED。

BLOCKED 和 WAITING 的区别也常考:BLOCKED 是抢锁失败自动进入、靠抢锁自动恢复;WAITING 是显式调用 wait/join/park 进入、靠 notify/unpark 恢复;TIMED_WAITING 就是 wait 的带超时版本,多了"自动超时"这条退出路径。

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

题目

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

我的回答

暂无回答记录

点评

评分:⭐⭐⭐☆☆(3/5)
核心问题:方法名报错(说成 isActive,应为 **isAlive**)
必背:- `isAlive()` → 底层 `state != NEW && state != TERMINATED` - **NEW 不算存活**(还没 start) - 存活 ≠ 正在运行(BLOCKED/WAITING/TIMED_WAITING 都算活)

完整讲解

第一部分:知识重点剖析

**核心考点定位**:考察对 `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 抛异常;线程管理框架在补充新线程前也会检查旧线程是否还在。

---

#4 synchronized锁的是什么? low java-concurrency

题目

synchronized锁的是什么?

我的回答

暂无回答记录

点评

评分:⭐⭐⭐⭐☆(4/5)
核心问题:静态 synchronized 表述不严谨(说成"整个类",实际是 Class<T>.class 单例对象)
必背:三种锁对象 - 非静态方法 → `this` - 静态方法 → `Class<T>.class`(全局单例) - `synchronized(obj)` → 显式对象 - 陷阱:`synchronized("foo")` 锁常量池 String,全 JVM 共享一把锁

完整讲解

第一部分:知识重点剖析

**核心考点定位**:区分 `synchronized` 三种用法各自锁定的对象,以及底层 monitor 机制,考察对 Java 内置锁粒度的理解。

**知识点拆解**:
1. **底层原理**:每个 Java 对象都有一个隐含的 **monitor**(内置锁),对象头(Mark Word)里存放锁状态和指向 monitor 的指针;字节码层面通过 `monitorenter` / `monitorexit` 指令加解锁
2. **设计权衡**:三种用法对应三种锁粒度——`this`(实例级)、`Class`(类级单例)、`显式对象`(自定义细粒度);粒度过粗影响并发度,过细影响正确性
3. **工程实践**:细粒度锁推荐声明 `private final Object lock = new Object()` 作为专用锁对象,避免锁共享导致意外的互斥

第二部分:考察方向与延伸递进

**考察方向**:
- **深度**:Mark Word 结构、Lock-Free → Thin Lock → Fat Lock 的状态位存储
- **广度**:与 `ReentrantLock` 显式锁的粒度对比
- **应用**:锁升级过程、锁粗化优化、伪共享

**延伸递进面试题**:
1. **为什么 static synchronized 锁的是 Class 对象而不是"类"?**
- 解答要点:Class 对象是 JVM 里每个类加载后的唯一实例,`MyClass.class` 就是那个对象引用;锁的是这个对象,天然全局共享
2. **锁 String 常量有什么坑?**
- 解答要点:`synchronized("foo")` 锁的是常量池里的 String,全 JVM 所有相同字面量的地方共享一把锁,容易造成全局串行化
3. **如何避免锁 `this` 带来的隐患?**
- 解答要点:`this` 会暴露给外部代码,外部可能拿到同一个对象做意外的同步;推荐私有 final 锁对象,只暴露必要的公共方法

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

面试官您好,`synchronized` 本质是给一个对象加内置锁(monitor),锁信息存在对象头里,通过 `monitorenter` 和 `monitorexit` 字节码指令实现。它有三种典型用法,锁的对象各不相同:

第一种,`synchronized` 修饰**非静态方法**,锁的是 `this`——当前实例对象,只有同一实例的多个线程才会互斥。

第二种,`synchronized` 修饰**静态方法**,锁的是**类的 Class 对象**,比如 `MyClass.class`。每个类在 JVM 里加载后都有一个唯一的 Class 实例,所以 static synchronized 相当于类级别的锁,所有实例共享一把锁。

第三种,`synchronized(obj) {}` 代码块,锁的是显式传入的 `obj`。工程实践中推荐用一个私有的 `final Object lock = new Object()` 作为专用锁,粒度可控、不会被外部拿到。

有几个坑需要注意:第一,锁 `this` 时如果外部代码能拿到这个对象,会形成意外的共享锁;第二,`synchronized("常量")` 锁的是常量池 String,全 JVM 共享一把锁,大坑;第三,static synchronized 因为锁的是 Class 对象,两个不同的实例对象也会互相阻塞——这是很多人忽略的。

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

题目

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

我的回答

暂无回答记录

点评

评分:⭐⭐⭐☆☆(3/5)
核心问题:- 时间点错:锁升级 JDK **1.5** 引入,不是 1.8 - **偏向锁现状漏了**:JDK 15 默认关闭、JDK 18 完全移除 - 术语错:"线程头"应为"对象头 Mark Word"
必背:无锁 → 偏向锁 → 轻量级锁 → 重量级锁;现代 JDK(16+)实际是"轻量级 → 重量级"两态

完整讲解

第一部分:知识重点剖析

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

---

#6 什么是AQS的独占模式和共享模式? low java-concurrency

题目

什么是AQS的独占模式和共享模式?

我的回答

暂无回答记录

点评

评分:⭐⭐⭐☆☆(3/5)
核心问题:拿 MySQL 读写锁类比不严谨(那是混合模式);没讲 AQS 核心(state、CLH 队列、tryAcquire vs tryAcquireShared)
必背:- 独占:ReentrantLock / WriteLock,state=持有次数,`tryAcquire` 返回 boolean - 共享:ReadLock / Semaphore / CountDownLatch,state=剩余许可,`tryAcquireShared` 返回 int - ReadWriteLock 是**双 AQS 实例组合**

完整讲解

第一部分:知识重点剖析

**核心考点定位**:考察对 AQS 模板方法设计的理解,尤其"独占"和"共享"两种模式在实现层面的差异,以及它们如何支撑上层各种同步组件。

**知识点拆解**:
1. **底层原理**:AQS 是模板方法模式的典型应用——它固定了"排队 + 阻塞/唤醒"这套骨架,把"如何判断能不能获得同步状态"这个差异点交给子类的 `tryAcquire/tryAcquireShared` 去实现。独占模式返回 boolean(0/1 二选一),共享模式返回 int(允许多个线程同时进入),语义差异决定了整个实现路径不同
2. **设计权衡**:独占模式简单直接但并发度低,适合"临界区必须串行"的场景;共享模式灵活但实现复杂、可能加剧队头唤醒问题,适合"临界区可以并发"的场景
3. **工程实践**:写锁优先的 ReadWriteLock 是共享模式的反面案例(读多写少用共享,写多时独占锁反而更优);Semaphore 是共享模式最纯粹的实现,用 `state` 存许可数

第二部分:考察方向与延伸递进

**考察方向**:
- **深度**:CLH 队列的入队/出队、`LockSupport.park/unpark` 的唤醒机制、独占锁的公平/非公平实现差异
- **广度**:与 `StampedLock` 的读无锁化对比;与 `synchronized` 的 JIT 优化对比
- **应用**:如何用 AQS 自定义一个信号量或读写锁;如何排查 AQS 相关的死锁/活锁

**延伸递进面试题**:
1. **AQS 的独占模式和共享模式能否同时用?**
- 解答要点:可以。`ReentrantReadWriteLock` 就是双 AQS 实例组合——WriteLock 用独占、ReadLock 用共享
2. **共享模式为什么要暴露 `tryAcquireShared` 的返回值(int)而不是 boolean?**
- 解答要点:共享模式需要表达"还剩余多少许可"以便下一个线程继续获取;独占模式只有"拿到/没拿到"两态,用 boolean 就够
3. **CountDownLatch 用的是独占还是共享?为什么?**
- 解答要点:共享模式。`state` 存倒数次数,多个线程同时 `await()` 都能进入等待队列;只有当 `state` 减到 0 时才唤醒所有等待者

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

面试官您好,AQS 是 Java 并发包的基础框架,本质是一个模板方法:它把"如何排队、如何阻塞、如何唤醒"这套骨架固定下来,让子类只实现"如何判断能否获得同步状态"这个差异点。核心数据结构是两个——一个是 `volatile int state` 表示同步状态,语义由子类决定;另一个是 CLH 双向链表存等待线程。

独占模式和共享模式的**核心区别**只有一个:能不能同时允许多个线程进入临界区。独占模式同一时刻只允许一个线程持有,`state` 的语义是"0 空闲、非0 持有次数",模板方法是 `tryAcquire` 和 `tryRelease`,返回 boolean 表示拿到/没拿到。典型组件是 `ReentrantLock` 和 `ReentrantReadWriteLock.WriteLock`。

共享模式允许多个线程同时进入,`state` 的语义是"还剩多少许可",模板方法是 `tryAcquireShared` 和 `tryReleaseShared`,返回 int 表示剩余量以便下一个线程继续。典型组件是 `ReentrantReadWriteLock.ReadLock`、`Semaphore`、`CountDownLatch`——比如 Semaphore 的 permits 数直接存在 `state` 里,p=3 就允许 3 个线程同时进入。

`ReentrantReadWriteLock` 其实是"独占+共享"的组合:两个独立的 AQS 实例,写锁用独占模式、读锁用共享模式。面试里如果把 MySQL 的读写锁类比为 AQS 独占/共享,会不严谨——MySQL 读写锁本身就是"混合模式",等价于 Java 的 ReadWriteLock。

选型上,如果临界区必须串行用独占模式,追求并发度用共享模式,读多写少用 ReadWriteLock。

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

题目

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

我的回答

暂无回答记录

点评

评分:⭐⭐☆☆☆(2/5)
核心问题:- **术语错**:说"一致性",实际是"**有序性**"(ordering) - 没讲三层结构(主内存 + 工作内存) - 没讲 JMM ≠ JVM(JMM 是规范,JVM 是实现)
必背:- 三大并发问题:原子性 / 可见性 / 有序性 - volatile 保证可见性+有序性;synchronized 保证三者;AtomicXxx 靠 CAS 保证原子性 - JMM ≠ JVM;MESI 是硬件缓存一致性协议,JMM 是软件抽象规范,互补

完整讲解

第一部分:知识重点剖析

**核心考点定位**:考察对 JMM 抽象本质的理解——它是一个**规范**而非实现,通过屏蔽硬件差异让 Java 代码在不同 CPU 上有可预测的并发语义。

**知识点拆解**:
1. **底层原理**:JMM 的核心是"主内存 ↔ 工作内存"的抽象。每个线程持有变量的副本存在工作内存里,线程只操作工作内存,不直接读写主内存。什么时候同步、什么时候刷新,由 JMM 规定的规则决定
2. **设计权衡**:如果没有 JMM,Java 代码在不同 CPU(x86 vs ARM)上会因为**指令重排、缓存一致性协议不同**表现完全不同。JMM 用"内存屏障 + happens-before"这套抽象把硬件差异屏蔽掉,代码写一次到处跑
3. **工程实践**:写并发代码前先问自己"这个变量需要保证什么"——只要求可见性用 volatile;要求原子性用 synchronized/AtomicXxx;要求复合原子操作用 ReentrantLock

第二部分:考察方向与延伸递进

**考察方向**:
- **深度**:MESI 协议 vs JMM 的差异、内存屏障的具体指令(StoreLoad、StoreStore、LoadLoad、LoadStore)、JIT 重排优化边界
- **广度**:volatile 的 happens-before 保证、double-check locking 的 volatile 版本、AtomicReference 的 CAS 无锁化
- **应用**:如何识别业务代码中的可见性问题;如何判断是否需要 volatile

**延伸递进面试题**:
1. **JMM 和 JVM 有什么本质区别?**
- 解答要点:JMM 是 JLS 定义的规范,JVM 是具体实现(HotSpot、OpenJ9)。JMM 屏蔽硬件差异,让不同 JVM 有兼容的并发语义
2. **为什么有了 MESI 协议还需要 JMM?**
- 解答要点:MESI 解决的是**缓存一致性**(多核 CPU 间缓存同步),JMM 解决的是**指令重排 + 可见性抽象**——MESI 是硬件层,JMM 是软件层,两者互补
3. **volatile 不能保证原子性,为什么 double-check 还需要它?**
- 解答要点:volatile 提供内存屏障,禁止指令重排——防止"new 出对象但字段未初始化就被其他线程看到"这种半初始化状态

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

面试官您好,Java 内存模型 JMM 是 JLS 定义的一套**抽象规范**,不是 JVM 本身。它的作用是**屏蔽底层硬件差异**,规定线程之间如何交换变量的值——让 Java 代码在不同 CPU 上有可预测的并发语义。

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

JMM 要解决的三大并发问题:**原子性**(i++ 是否被拆)、**可见性**(线程 A 改了 B 能否看到)、**有序性**(指令能否被重排)。对应的手段是:volatile 解决可见性和有序性(靠内存屏障),synchronized 同时解决三者(靠 monitor 机制 + happens-before 规则),AtomicXxx 靠 CAS 解决原子性。

几个容易搞混的点:第一,JMM ≠ JVM,JMM 是规范、JVM 是实现;第二,MESI 是硬件缓存一致性协议,JMM 是软件规范,两者互补而非替代;第三,volatile 保证可见性和有序性但**不保证原子性**,比如 `count++` 不能用 volatile;第四,"一致性"是 CAP/事务领域概念,JMM 用的是"有序性"这个术语。

#8 volatile能保证原子性吗?为什么? medium java-concurrency

题目

volatile能保证原子性吗?为什么?

我的回答

暂无回答记录

点评

评分:⭐⭐⭐⭐☆(4/5)
核心问题:底层机制说浅了(是内存屏障 + MESI,不是简单"每次读主存")
必背:- volatile 语义边界是"**单个变量的一次读写**",不是"一段代码" - `i++` 拆成读→加→写三步,每步独立可见但中间可能被插入 → 丢失更新 - volatile 写后插 StoreLoad 屏障、读前插 LoadLoad 屏障 - AtomicXxx = volatile 变量 + 无锁 CAS 循环,互补

完整讲解

第一部分:知识重点剖析

**核心考点定位**:考察对 volatile 语义边界的深刻理解——它只做"单变量读写的可见性 + 有序性",**不做临界区互斥**,因此不能保证复合操作的原子性。

**知识点拆解**:
1. **底层原理**:volatile 靠**内存屏障**实现——写操作后插入 `StoreLoad` 屏障强制刷缓存到主内存;读操作前插入 `LoadLoad` 屏障禁止后续读操作提前执行。硬件层对应 MESI 协议,volatile 写会广播让其他核缓存失效。整个过程是"**单变量的读写原子性**",不是"一段代码的临界区"
2. **设计权衡**:volatile 比 synchronized 便宜(无锁、无上下文切换),但能力边界窄——只适合"读一次/写一次"的场景;一旦涉及"读-改-写"复合操作,就必须上锁或 CAS
3. **工程实践**:volatile 的正确用法是**状态标志位**(如循环开关)和**发布共享引用**(如单例、Handler);不要用 volatile 变量做计数器,`AtomicLong` / `LongAdder` 才是正解

第二部分:考察方向与延伸递进

**考察方向**:
- **深度**:内存屏障的四种类型(LoadLoad、StoreStore、LoadStore、StoreLoad)、MESI 协议的 E/I 状态、伪共享对 volatile 的影响
- **广度**:与 `synchronized`、`AtomicXxx`、`LongAdder` 的选型对比
- **应用**:double-check locking 的 volatile 单例、volatile 用于线程循环标志、发布共享引用的经典用法

**延伸递进面试题**:
1. **volatile 能保证"单个变量读写的原子性",这是真的吗?**
- 解答要点:严格来说,volatile 保证的是"读写可见性",单变量 32/64 位读写的原子性由 CPU 硬件保证,但 volatile 提供了语义保证
2. **double-check 单例为什么必须加 volatile?**
- 解答要点:`new Singleton()` 被编译成三步——分配内存、初始化字段、赋值引用;如果不加 volatile,JIT 可能重排为"分配 → 赋值 → 初始化",导致其他线程拿到半初始化对象
3. **volatile 和 CAS 的区别?为什么 AtomicXxx 用 volatile + CAS?**
- 解答要点:volatile 保证可见性 + 有序性,CAS 保证原子性;AtomicXxx 内部用 volatile 变量 + 无锁 CAS 循环,两者互补

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

面试官您好,volatile **不能保证原子性**,这个结论是明确的。

volatile 只保证**可见性**和**有序性**。可见性的原理是 volatile 写操作后 JVM 插入 `StoreLoad` 内存屏障,强制把缓存刷回主内存,其他线程的 CPU 通过 MESI 协议让本地缓存失效,从而看到最新值;有序性是 volatile 读写两侧都插入屏障,禁止 JIT 和 CPU 把 volatile 操作前后的指令重排。

为什么不能保证原子性?因为 volatile 的语义边界是"单个变量的一次读写",不是"一段代码块"。像 `i++` 这种复合操作会被编译成三步:读取 i、加 1、写回 i,每步都是独立的读写,volatile 只能保证每一步对主内存可见,但**中间可能被其他线程插入**——比如线程 A 读到 i=0,线程 B 也读到 i=0,A 写回 1,B 也写回 1,最终结果是 1 而不是 2,这就是"丢失更新"。

要保证复合操作的原子性,需要用锁(synchronized / ReentrantLock)或者 CAS(AtomicXxx)。AtomicXxx 内部就是"volatile 变量 + 无锁 CAS 循环"——volatile 保证可见性,CAS 保证原子性,两者互补。

volatile 的正确用法是"读一次/写一次"的场景:状态标志位(`volatile boolean running`)、double-check 单例(防止 new 对象的重排导致半初始化)、发布共享引用(如 Handler)。不要用 volatile 做计数器,用 `AtomicLong` 或高并发下的 `LongAdder`。

一句话总结:**volatile 保证"读到的是最新的",不保证"整个操作不被打断"**。

#9 happens-before和as-if-serial有啥区别和联系? medium java-concurrency

题目

happens-before和as-if-serial有啥区别和联系?

我的回答

暂无回答记录

点评

评分:未作答

完整讲解

第一部分:知识重点剖析

**核心考点定位**:考察对 JMM 两大规则边界的理解——一个管"跨线程的可见性契约",一个管"单线程内的优化自由",两者互补但作用域严格区分。

**知识点拆解**:
1. **底层原理**:`as-if-serial` 是 JIT 编译器和 CPU 的**优化授权书**——它告诉优化器"你可以任意重排,只要单线程观察到的结果与源码一致就行"。`happens-before` 是 JMM 给出的**跨线程可见性承诺**——它告诉开发者"满足这些规则的操作,前面的结果后面一定能看到"
2. **设计权衡**:如果只有 as-if-serial 没有 happens-before,JIT 可以疯狂重排,单线程没问题但并发时结果全乱;如果只有 happens-before 没有 as-if-serial,JIT 无法自由优化,性能崩塌。两者组合才能既高效又正确
3. **工程实践**:写单线程代码不需要担心重排(as-if-serial 兜底);写并发代码必须确保共享变量有 happens-before 关系(volatile/synchronized/AtomicXxx)

第二部分:考察方向与延伸递进

**考察方向**:
- **深度**:as-if-serial 的判定算法(数据依赖分析)、happens-before 的传递性、7 条规则的字节码对应
- **广度**:与 `volatile` 内存屏障的对应关系、与 `synchronized` 的 happens-before 保证
- **应用**:识别业务代码中"缺失 happens-before 关系"的并发 bug

**延伸递进面试题**:
1. **为什么 double-check 单例必须加 volatile?**
- 解答要点:`new Singleton()` 编译成三步(分配内存、初始化字段、赋值引用),as-if-serial 允许重排成"分配 → 赋值 → 初始化",其他线程看到非 null 但字段未初始化;加 volatile 建立 happens-before 关系
2. **单线程内为什么不需要 volatile?**
- 解答要点:单线程受 as-if-serial 约束,重排不能改变观察结果;volatile 是为了跨线程可见性,单线程场景多余
3. **happens-before 是"操作真实发生的先后顺序"吗?**
- 解答要点:不是。happens-before 只是**可见性契约**,不保证物理时间顺序;即使 A hb B,CPU 也可能先执行 B 再执行 A,但保证 A 的结果 B 能看到

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

面试官您好,happens-before 和 as-if-serial 都是 JMM 的规则,但作用域完全不同。

as-if-serial 是**单线程内**的优化自由——它告诉 JIT 和 CPU:"你可以随便重排指令,只要**单线程观察到的结果**和源码顺序一致就行"。比如 `int a=0; a=1; println(a);` 打印一定是 1,即使 JIT 重排也不能改这个观察结果。但 as-if-serial **不管跨线程的情况**——如果两个线程共享变量,单线程的优化结果其他线程看不到。

happens-before 是**跨线程**的可见性契约——JMM 给出的 7 条规则保证"满足 hb 关系的前面操作,后面操作一定能看到结果"。这 7 条包括程序顺序、监视器锁、volatile 读写、线程启动、线程终止、线程中断、对象终结。比如 volatile 写 happens-before 后续 volatile 读,保证写的值一定可见。

两者的联系是**互补关系**:as-if-serial 是"单线程内允许优化但保证结果一致",happens-before 是"跨线程保证可见性"。写单线程代码不需要 volatile(as-if-serial 兜底);写并发代码必须建立 happens-before 关系(用 volatile / synchronized / AtomicXxx)。

常见坑是 double-check 单例:`new Singleton()` 编译成"分配内存 → 初始化字段 → 赋值引用"三步,as-if-serial 允许重排成"分配 → 赋值 → 初始化",其他线程看到非 null 但字段未初始化。加 volatile 后建立 happens-before,禁止这种重排。

一句话总结:**as-if-serial 让单线程代码随便优化,happens-before 让跨线程代码有可见性保证**。

#10 到底啥是内存屏障?到底怎么加的? medium java-concurrency

题目

到底啥是内存屏障?到底怎么加的?

我的回答

暂无回答记录

点评

评分:⭐⭐☆☆☆(2/5)
核心问题:- "内核的核心指令"错——是 **CPU 指令**,不是 OS 内核 - 完全没讲四种类型 - 没说"怎么加":JIT 编译器显式插入,不是硬件自动插入
必背:- 四种屏障:LoadLoad / StoreStore / LoadStore / LoadStore - 插入位置:volatile 写前 StoreStore / 写后 StoreLoad / 读前 LoadLoad;synchronized 解锁后 StoreStore + StoreLoad - x86 强内存模型(`lock add` 锁总线,开销大);ARM 弱内存模型(`dmb`,开销小但重排更自由) ---

完整讲解

第一部分:知识重点剖析

**核心考点定位**:考察对内存屏障作为 JMM 底层实现机制的精确理解——它是 CPU 指令,由编译器/JIT 插入,用来禁止指令重排和强制缓存同步。

**知识点拆解**:
1. **底层原理**:现代 CPU 为了性能会做两类优化——**CPU 乱序执行**(在多个执行单元并行执行指令)和 **JIT 编译器重排**(改变指令顺序以利用数据依赖)。内存屏障是**指令级的"禁止重排屏障"**,插入位置由 JMM 规则决定,硬件负责在屏障两侧强制顺序执行并同步缓存
2. **设计权衡**:屏障开销大小与并发正确性绑定——x86 强内存模型天然禁止 LoadStore,volatile 只需 1 条 `lock add`;ARM 弱内存模型需要更多屏障,但每条更便宜。JMM 抽象掉了这些差异,Java 代码无需关心
3. **工程实践**:一般开发不用手动加屏障;理解屏障的价值在于**理解 volatile / synchronized / final 为什么能工作**,以及在极高性能场景(如 Netty 的 `Unsafe`、Disruptor)中如何绕过 JMM

第二部分:考察方向与延伸递进

**考察方向**:
- **深度**:MESI 协议与屏障的关系、x86 `lock` 前缀锁总线原理、ARM `dmb` 指令的具体变体
- **广度**:与 `volatile`、`final`、`synchronized` 的对应关系;与 `Unsafe.putOrdered` 的部分写入屏障
- **应用**:如何判断业务代码是否需要屏障(跨线程共享就必须要);如何在 JDK 16+ 用 `VarHandle` 精细控制屏障

**延伸递进面试题**:
1. **x86 和 ARM 的内存屏障差异?**
- 解答要点:x86 天然禁止 LoadStore,volatile 只需 `lock add`;ARM 需要显式插入 `dmb` 屏障,重排更自由但屏障更便宜
2. **final 字段为什么也需要屏障?**
- 解答要点:构造完成后插入 StoreStore + LoadStore,保证 final 字段初始化完成后对其他线程"发布"时值不变,防止 double-checked 模式的重排问题
3. **内存屏障会阻塞 CPU 吗?**
- 解答要点:不阻塞。屏障只是禁止重排 + 强制缓存同步,数据通道照常流动,只是 CPU 不再"投机执行"屏障两侧的操作

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

面试官您好,内存屏障是**CPU 指令层的指令**,不是操作系统内核概念,也不是硬件自动插入的。它是 JIT 编译器在特定位置**显式插入的指令序列**,用来**禁止指令重排 + 强制缓存同步**。

现代 CPU 有两种优化:**CPU 乱序执行**(在多个执行单元并行跑指令)和 **JIT 重排**(编译器改变指令顺序利用数据依赖)。屏障就是在关键位置插一根"栏杆",禁止栏杆两侧的操作被重排到一起。

一共有四种屏障:**LoadLoad、StoreStore、LoadStore、LoadStore**。每种对应一类禁止的重排,比如 StoreLoad 禁止"后续 Load 提前到前一个 Store 之前"。x86 硬件天然禁止 LoadStore,所以 x86 的屏障开销相对简单——volatile 写只需 `lock add` 一条指令锁总线;ARM 是弱内存模型,重排更自由,需要显式插入 `dmb` 屏障。

JMM 规定了屏障插入的位置:**volatile 写前**插 StoreStore、**volatile 写后**插 StoreLoad(保证立即刷缓存到主内存)、**volatile 读前**插 LoadLoad;**synchronized 解锁后**插 StoreStore + StoreLoad(保证锁内操作在解锁前完成,其他线程加锁可见);**final 字段构造完成后**插 StoreStore + LoadStore(保证 final 字段初始化后发布时值不变)。

所以 volatile 的可见性 = 内存屏障 + MESI 协议(缓存同步);volatile 的有序性 = 屏障禁止重排;synchronized 的枷锁语义 = 屏障保证锁内操作的可见性。

x86 上 volatile 写是 `lock` 前缀指令会锁总线,开销大但简单;ARM 上只需 `dmb ish`,开销小但重排更自由。屏障不是"卡住 CPU",是"禁止重排 + 刷缓存",数据通道照常流动。

📚 查看全部面试记录