深入理解Java虚拟机:JVM高级特性与最佳实践(第3版)第十二章知识点问答(15题)
问题1
在多线程环境下,Java 内存模型(JMM)抽象出了哪两种内存?它们之间的关系是什么?
(提示:一个是线程共享的区域,一个是线程私有的区域)
在 JMM 中,抽象出了两种内存:
-
主内存(Main Memory)
- 所有线程共享,存放实例字段、静态字段、数组元素等。
- 类似于“公共存储区”。
-
工作内存(Working Memory)
- 每个线程私有,存放从主内存拷贝的变量副本。
- 线程对变量的所有操作(读写等)必须先在工作内存中进行,之后再同步回主内存。
关系:
- 线程之间不能直接访问对方的工作内存,只能通过 主内存 作为中介来实现通信。
- 这就解释了为什么多线程程序需要 同步机制(如
volatile
、synchronized
)来保证可见性和有序性。
问题2
JMM 规定了 主内存与工作内存之间变量传递的 8 种原子操作。请问它们分别是什么?
JMM 规定了 主内存与工作内存交互的 8 种原子操作:
- lock(锁定):作用于主内存变量,标识变量被线程独占。
- unlock(解锁):作用于主内存变量,释放锁定。
- read(读取):作用于主内存变量,把值从主内存传输到工作内存,供后续 load 使用。
- load(载入):作用于工作内存变量,把 read 得到的值放入线程的工作内存变量副本中。
- use(使用):作用于工作内存变量,把变量值传给执行引擎。
- assign(赋值):作用于工作内存变量,把执行引擎得到的值赋给工作内存的变量副本。
- store(存储):作用于工作内存变量,把变量的值传回主内存,供后续 write 使用。
- write(写入):作用于主内存变量,把 store 传来的值写入主内存。
关系:
read → load
和store → write
必须成对出现。use
和assign
仅在工作内存中发生。lock/unlock
与主内存变量的同步机制有关。
👉 总结:
这八种操作定义了 线程如何与主内存同步变量值,并确保在 JMM 中的可见性和原子性。
问题3
为什么说 volatile 变量 在 JMM 中有两层语义保证?它们分别是什么?
在 JMM 中,volatile 变量 有两层语义保证:
-
可见性(Visibility)
- 对一个 volatile 变量的修改,都会立刻刷新到主内存。
- 读 volatile 变量时会强制从主内存获取最新值,而不是从工作内存缓存。
- 因此,所有线程都能“立即看到” volatile 变量的最新变化。
-
有序性(Ordering)
- volatile 禁止指令重排序(针对该变量的读写)。
- 在字节码层面,编译器会在 volatile 读/写操作前后插入 内存屏障(Memory Barrier),保证在它前后的指令不会乱序执行。
❌ 注意:
- volatile 不保证操作的原子性(例如
count++
仍然不是线程安全的),只能保证单次读/写的原子性。
问题4
相比 volatile,synchronized 在 JMM 中额外提供了什么保证?它是如何通过 lock/unlock 操作来实现的?
在 JMM 中,synchronized 的语义比 volatile 更强,提供了:
-
原子性(Atomicity)
- 一个同步块内的代码,在同一时刻只能由一个线程执行。
- 保证了“操作不可分割”,避免了并发冲突。
-
可见性(Visibility)
- 线程在进入同步块(lock 操作)前,必须先清空工作内存,从主内存中重新加载变量。
- 线程在退出同步块(unlock 操作)时,必须把对变量的修改刷新回主内存。
-
有序性(Ordering)
-
同步块的进入和退出建立了严格的 happens-before 关系:
- 解锁(unlock)happens-before 随后对同一把锁的加锁(lock)。
-
确保临界区内代码不会因指令重排序而影响同步语义。
-
实现方式(基于 JMM 的 8 种原子操作):
- lock:线程获得锁,清空工作内存对应变量。
- unlock:线程释放锁,强制将修改后的变量值刷新到主内存。
- 通过这两个操作,synchronized 确保了 互斥 + 内存可见性 + 有序性。
👉 总结:
- volatile:保证可见性 + 有序性,不保证复合操作的原子性。
- synchronized:保证原子性 + 可见性 + 有序性,是更强的同步机制。
问题5
JMM 提出了 happens-before 原则 来帮助开发者理解并发语义。请列出至少三条 happens-before 规则,并解释它们的含义。
JMM 的 happens-before 原则 定义了哪些操作在并发环境下保证可见性和顺序性。常见规则包括:
-
程序次序规则
- 在一个线程内,代码按程序顺序执行。
- 即使编译器/CPU 有重排序优化,也不会改变单线程语义。
-
监视器锁定规则
- 对一个锁的解锁(unlock)happens-before 随后对该锁的加锁(lock)。
- 确保临界区内修改对后续持有同一锁的线程可见。
-
volatile 变量规则
- 对一个 volatile 变量的写操作 happens-before 后续对该变量的读操作。
- 确保对 volatile 的修改能被立即看见。
-
线程启动规则
- 在线程 A 启动线程 B(调用
Thread.start()
)之前,A 对共享变量的修改对 B 可见。
- 在线程 A 启动线程 B(调用
-
线程终止规则
- 一个线程的所有操作 happens-before 另一个线程检测到该线程结束(通过
Thread.join()
、Thread.isAlive()
等)。
- 一个线程的所有操作 happens-before 另一个线程检测到该线程结束(通过
-
线程中断规则
- 对线程调用
interrupt()
happens-before 线程检测到中断事件(通过interrupted()
、isInterrupted()
)。
- 对线程调用
-
对象终结规则
- 对象的构造函数执行结束 happens-before 它的 finalize() 方法开始。
👉 总结:
happens-before 原则给出了“哪些操作的结果对其他线程可见”的契约。只要程序能建立 happens-before 关系,就能保证线程间的内存可见性和顺序性。
问题6
为什么说 JMM 是一种语言级别的内存模型?它与具体硬件的内存模型之间是什么关系?
-
JMM 是语言级内存模型
- JMM(Java Memory Model)是 Java 语言规范的一部分。
- 它规定了 Java 程序在并发环境下的 可见性、有序性、原子性 语义。
- 开发者写并发代码时只需遵守 JMM 的规则,而不用直接面对底层硬件的复杂性。
-
与硬件内存模型的关系
-
不同 CPU 架构有不同的内存模型:
- x86:相对强(大多数情况下保证顺序一致)。
- ARM/PowerPC:更弱(允许更多乱序执行)。
-
JMM 的任务:在不同硬件模型之上提供 统一的抽象。
-
HotSpot 等 JVM 会在需要的地方插入 内存屏障(Memory Barrier),用来限制指令重排,从而让实际执行符合 JMM 的规定。
-
👉 总结:
- JMM 是语言级规范,面向 Java 开发者。
- 硬件内存模型是底层实现,面向 CPU 架构。
- JVM 在中间起桥梁作用:通过编译器和运行时指令翻译,把 JMM 的 happens-before 等规则落实到底层汇编指令上。
问题7
JMM 对 long/double 类型的特殊规定是什么?为什么会出现所谓的 “非原子性问题”?
在 JMM 中,long 和 double 的非原子性协定是:
-
对于未被 volatile 修饰的 64 位数据类型(long、double),JVM 允许将一次 load/store/read/write 操作拆分为 两个 32 位操作来执行。
-
这样做的原因:
- 早期的 32 位 JVM/CPU 平台无法保证一次性原子地读写 64 位数据。
- 因此规范允许 JVM 自行选择是否保证这些操作的原子性。
可能的问题:
- 在多线程环境下,一个线程正在写 long/double 时,另一个线程可能读到 “高 32 位是新值,低 32 位是旧值” 这样的“半个值”。
- 这就是所谓的 long/double 非原子性问题。
解决方法:
- 使用 volatile 修饰 long/double → JMM 要求保证其读写原子性。
- 或者通过 synchronized 来保证互斥访问。
👉 总结:
- JMM 的非原子性协定:未加 volatile 的 long/double 可能被拆成两次 32 位操作。
- 后果:可能读到“拆开的值”。
- 解决办法:加 volatile 或加锁来确保原子性。
问题8
为什么说 JMM 保证了可见性和有序性,但并不保证原子性?请结合 i++
的例子说明。
-
JMM 的保证范围
- JMM 保证了 可见性(修改会及时写回主内存)和 有序性(通过 happens-before 规则禁止某些重排序)。
- 但 JMM 并不保证复合操作的原子性。
-
i++
的拆解过程
在字节码层面,i++
通常分为三步:- load i:读取 i 的值到工作内存/寄存器。
- add 1:在执行引擎中完成加法运算。
- store i:将新值写回工作内存,再同步到主内存。
如果有两个线程同时执行
i++
,可能发生:- 线程 A 读取了 i = 10;
- 线程 B 也读取了 i = 10;
- 两个线程各自计算
i+1 = 11
,并写回主内存; - 最终结果是 i = 11,而不是 12。
-
为什么 JMM 不保证原子性
- 原子性保证需要更强的同步机制(如锁),会降低性能。
- JMM 的设计目标是 最小化限制,只保证可见性和有序性,原子性交由程序员通过
synchronized
、Lock
或AtomicInteger
等工具保证。
👉 总结:
volatile
变量虽然能保证可见性,但不能保证i++
的原子性。- 要保证
i++
的原子性,必须使用 synchronized 或 *原子类(Atomic)**。
问题9
请解释一下 Java 内存模型中的“先行发生(happens-before)”与“程序次序(as-if-serial)”的区别与联系。
-
as-if-serial 语义
-
是单线程程序的保证:不论编译器和 CPU 如何优化,程序执行结果必须与按程序顺序执行的结果一致。
-
它允许重排序,但只要不改变单线程的最终结果,就被认为等价。
-
举例:
int a = 1; int b = 2;
编译器可以先赋值 b,再赋值 a,因为最终结果一致。
-
-
happens-before 关系
-
是多线程环境下的保证:前一个操作的结果对后续操作可见,并且前一个操作的执行顺序排在后一个之前。
-
它定义了并发语义下的可见性与有序性约束(比如 volatile 写 → volatile 读;unlock → lock)。
-
举例:
- 线程 A 写 volatile 变量 → 线程 B 随后读该变量,A 的写对 B 可见。
-
-
两者联系
- as-if-serial 是单线程的保证,确保优化不会改变程序逻辑结果。
- happens-before 是跨线程的保证,确保并发程序的执行符合预期的可见性/顺序性。
- 二者结合起来,构成了 Java 内存模型对单线程和多线程的完整语义约束。
👉 总结:
- as-if-serial = 单线程结果一致性保证。
- happens-before = 多线程间的可见性和顺序性保证。
问题10
在 JMM 中,final 字段 有一条特殊的语义。请问它的作用是什么?它是如何保证对象构造安全性的?
(提示:和 “安全发布” 有关,关键点是对象构造过程中的写 → 读顺序)
在 JMM 中,final 字段的特殊语义 是:
-
禁止重排序
-
对 final 字段的写入操作,必须在构造函数结束前完成,不能与对象引用的赋值重排序。
-
换句话说:
class A {final int x;A() {x = 42; // 写 final 字段} }
JMM 保证
x=42
一定在对象引用“this”被发布之前完成。
-
-
安全发布
- 一旦构造函数执行完毕,其他线程通过正确方式(如对象引用)访问这个对象时,一定能看到 final 字段的正确值,而不会读到默认值(0/null)。
- 这保证了 final 字段的“初始化安全性”。
-
区别于普通字段
- 普通字段的写入可能与对象引用赋值发生重排序,从而导致其他线程读到未初始化的值。
- final 字段则通过 JMM 的额外规则避免了这种情况。
👉 总结:
- 语法层面:final 修饰引用时,引用本身不可变,但对象内容可变。
- JMM 层面:final 字段有 写屏障 语义,保证构造完成后被安全发布,其他线程一定能读到初始化后的值。
问题11
JMM 如何通过 内存屏障(Memory Barrier) 来落实到底层硬件?请举例说明读写屏障分别的作用。
在 JMM 中,内存屏障(Memory Barrier / Fence) 是实现 happens-before 语义的底层手段。
-
概念
-
内存屏障是一类特殊的 CPU 指令,作用是:
- 禁止特定的指令重排序。
- 刷新处理器缓存,保证数据在多核环境下可见。
-
JVM 在 volatile/synchronized 等地方插入屏障,来把 JMM 规则落实到不同 CPU 架构。
-
-
常见类型(在 JMM 的抽象里主要有 4 种,实际 CPU 实现略有不同):
- LoadLoad 屏障:保证前面的读操作完成后,后续读操作才能执行。
- StoreStore 屏障:保证前面的写操作对其他处理器可见后,后续写操作才能执行。
- LoadStore 屏障:保证前面的读完成后,后续写才能执行。
- StoreLoad 屏障:最强的屏障,保证前面的写对所有处理器可见后,后续读才能执行(通常会导致 CPU pipeline flush)。
-
应用示例
- volatile 写操作:在写之前插入 StoreStore,在写之后插入 StoreLoad → 保证写的可见性 + 禁止后续读/写乱序。
- volatile 读操作:在读之前插入 LoadLoad,在读之后插入 LoadStore → 保证读能获取到主内存最新值,并禁止与后续操作乱序。
👉 总结:
- 内存屏障 = JVM 在底层 CPU 指令级别的“防护栅栏”。
- 它保证了 JMM 的可见性和有序性 能在不同硬件(x86、ARM、PowerPC)上都生效。
问题12
在 JMM 的 happens-before 规则中,有三条是专门针对 线程操作 的。请分别说明:
- 线程启动规则
- 线程终止规则
- 线程中断规则
在 JMM 的 happens-before 规则里,关于线程有三条专门规定:
-
线程启动规则
- 在 主线程调用
Thread.start()
之前,对共享变量的修改, - 对启动的子线程 是可见的。
- 保证子线程能看到父线程在启动前完成的初始化操作。
- 在 主线程调用
-
线程终止规则
- 一个线程中的所有操作,
- happens-before 另一个线程成功从
Thread.join()
返回,或检测到该线程已终止(如isAlive()==false
)。 - 保证父线程能看到子线程执行完的结果。
-
线程中断规则
- 调用
Thread.interrupt()
, - happens-before 被中断线程检测到中断事件(通过
interrupted()
或isInterrupted()
)。 - 保证中断信号不会丢失,线程能及时感知。
- 调用
👉 总结:
- start() → 子线程操作可见
- 子线程操作 → join()/isAlive 可见
- interrupt() → 中断检测可见
下一题(问题13)
除了线程规则外,JMM 还规定了一条 对象终结(finalize)规则。请问它的含义是什么?
在 JMM 中,关于 对象终结(finalize) 有一条 happens-before 规则:
- 一个对象的构造函数执行结束,
happens-before 该对象的 finalize() 方法的开始。
含义:
- 保证在
finalize()
方法里看到的对象状态,至少包含 构造函数完成时的结果。 - 避免了对象在 finalize 中读到“未初始化字段”的情况。
👉 总结:
-
finalize 的 happens-before 规则确保了 对象构造 → 对象终结 之间的顺序性。
-
不过需要注意:
finalize()
只会被 JVM 至少调用一次,但调用时机不确定。- 在现代 Java 开发中,
finalize()
已被标记为 不推荐使用,替代方案是try-with-resources
或Cleaner
。
问题14
JMM 的规则不仅体现在语言层面,还影响了并发工具的实现。请举例说明 JMM 在 并发工具类(如 Lock、AQS、原子类) 中的体现。
-
原子类(AtomicXXX)
- 基于 CAS(Compare-And-Swap) 实现无锁操作。
- CAS 本质依赖 JMM 的 volatile 变量保证可见性,以及 CPU 的原子指令保证比较和更新的原子性。
- 例如:
AtomicInteger.incrementAndGet()
内部用Unsafe.compareAndSwapInt()
。
-
Lock 和 AQS(AbstractQueuedSynchronizer)
-
AQS 中的 state 字段 通常用 volatile 修饰,保证不同线程能立即看到锁状态的变化。
-
加锁与解锁操作通过 happens-before 规则 建立可见性和有序性:
- unlock() happens-before lock()。
-
线程排队依赖 CAS 来竞争锁状态。
-
-
并发集合 / 阻塞队列
- 内部大量使用 volatile + CAS 保证队列节点(指针/next)的安全修改。
- 这就是你说的“双指针”思想:通过 CAS 更新 head/tail 指针,保证链表/队列并发安全。
-
synchronized 的实现
- JVM 层面直接依赖 JMM 的 lock/unlock 语义,确保原子性、可见性和有序性。
👉 总结:
- CAS + volatile + happens-before 是 Java 并发工具的三大基石。
- JMM 提供了 内存可见性和有序性保证,让高层并发工具能安全高效地运行。
问题15
请系统总结 第 12 章《Java 内存模型与线程》 的主要知识点:
- JMM 抽象模型(主内存 / 工作内存)
- 内存交互的 8 种操作
- volatile 与 synchronized 的语义
- happens-before 原则及其规则
- 特殊语义(long/double、final 字段)
- JMM 与硬件模型的关系
- 并发工具的实现基础
-
JMM 抽象模型
- 主内存(Main Memory):所有线程共享,保存实例字段、静态字段、数组元素。
- 工作内存(Working Memory):线程私有,存放主内存变量的副本,线程操作必须先拷贝到工作内存再执行。
- 线程之间的通信必须经过主内存,工作内存不可直接共享。
-
内存交互的 8 种原子操作
lock / unlock
(锁定、解锁)read / load
(从主内存读取 → 拷贝到工作内存)use / assign
(在工作内存使用/赋值变量)store / write
(把工作内存值写回主内存)- 这八种操作定义了线程与主内存交互的规则。
-
volatile 的语义
- 可见性:写入立即刷新主内存,读时强制从主内存获取。
- 有序性:禁止 volatile 前后的指令重排序(通过内存屏障实现)。
- 不保证原子性:
i++
等复合操作仍然需要锁或原子类。
-
synchronized 的语义
- 原子性:临界区互斥访问。
- 可见性:解锁前必须刷新主内存,加锁后必须清空工作内存。
- 有序性:
unlock
happens-before 后续的lock
。
-
happens-before 原则
-
程序次序:单线程内代码按顺序执行。
-
锁定规则:unlock → lock。
-
volatile 规则:写 → 读。
-
线程规则:
- start() → 子线程操作
- 子线程操作 → join()/isAlive()
- interrupt() → 中断检测
-
对象终结规则:构造结束 → finalize()。
-
-
特殊语义
- long/double 非原子性协定:未加 volatile 的 64 位变量读写可能被拆分为两次 32 位操作。
- final 字段语义:构造函数中对 final 字段的写入禁止与 this 引用重排序,保证对象发布安全。
-
JMM 与硬件模型的关系
- 不同硬件内存模型差异大(x86 较强,ARM/PowerPC 更弱)。
- JVM 通过插入 内存屏障(Memory Barrier),将 JMM 的规则落实到底层 CPU 指令。
-
并发工具的实现基础
- 原子类:依赖 CAS + volatile 保证原子性和可见性。
- AQS / Lock:依赖 volatile 状态字段 + CAS 实现锁语义,happens-before 保证可见性。
- 并发容器:广泛使用 volatile + CAS 更新节点指针(双指针算法)。