当前位置: 首页 > news >正文

深入理解Java虚拟机:JVM高级特性与最佳实践(第3版)第十二章知识点问答(15题)

问题1

在多线程环境下,Java 内存模型(JMM)抽象出了哪两种内存?它们之间的关系是什么?

(提示:一个是线程共享的区域,一个是线程私有的区域)


在 JMM 中,抽象出了两种内存:

  1. 主内存(Main Memory)

    • 所有线程共享,存放实例字段、静态字段、数组元素等。
    • 类似于“公共存储区”。
  2. 工作内存(Working Memory)

    • 每个线程私有,存放从主内存拷贝的变量副本。
    • 线程对变量的所有操作(读写等)必须先在工作内存中进行,之后再同步回主内存。

关系:

  • 线程之间不能直接访问对方的工作内存,只能通过 主内存 作为中介来实现通信。
  • 这就解释了为什么多线程程序需要 同步机制(如 volatilesynchronized)来保证可见性和有序性。

问题2

JMM 规定了 主内存与工作内存之间变量传递的 8 种原子操作。请问它们分别是什么?


JMM 规定了 主内存与工作内存交互的 8 种原子操作

  1. lock(锁定):作用于主内存变量,标识变量被线程独占。
  2. unlock(解锁):作用于主内存变量,释放锁定。
  3. read(读取):作用于主内存变量,把值从主内存传输到工作内存,供后续 load 使用。
  4. load(载入):作用于工作内存变量,把 read 得到的值放入线程的工作内存变量副本中。
  5. use(使用):作用于工作内存变量,把变量值传给执行引擎。
  6. assign(赋值):作用于工作内存变量,把执行引擎得到的值赋给工作内存的变量副本。
  7. store(存储):作用于工作内存变量,把变量的值传回主内存,供后续 write 使用。
  8. write(写入):作用于主内存变量,把 store 传来的值写入主内存。

关系:

  • read → loadstore → write 必须成对出现。
  • useassign 仅在工作内存中发生。
  • lock/unlock 与主内存变量的同步机制有关。

👉 总结:
这八种操作定义了 线程如何与主内存同步变量值,并确保在 JMM 中的可见性和原子性。


问题3

为什么说 volatile 变量 在 JMM 中有两层语义保证?它们分别是什么?


在 JMM 中,volatile 变量 有两层语义保证:

  1. 可见性(Visibility)

    • 对一个 volatile 变量的修改,都会立刻刷新到主内存。
    • 读 volatile 变量时会强制从主内存获取最新值,而不是从工作内存缓存。
    • 因此,所有线程都能“立即看到” volatile 变量的最新变化。
  2. 有序性(Ordering)

    • volatile 禁止指令重排序(针对该变量的读写)。
    • 在字节码层面,编译器会在 volatile 读/写操作前后插入 内存屏障(Memory Barrier),保证在它前后的指令不会乱序执行。

❌ 注意:

  • volatile 不保证操作的原子性(例如 count++ 仍然不是线程安全的),只能保证单次读/写的原子性。

问题4

相比 volatile,synchronized 在 JMM 中额外提供了什么保证?它是如何通过 lock/unlock 操作来实现的?


在 JMM 中,synchronized 的语义比 volatile 更强,提供了:

  1. 原子性(Atomicity)

    • 一个同步块内的代码,在同一时刻只能由一个线程执行。
    • 保证了“操作不可分割”,避免了并发冲突。
  2. 可见性(Visibility)

    • 线程在进入同步块(lock 操作)前,必须先清空工作内存,从主内存中重新加载变量。
    • 线程在退出同步块(unlock 操作)时,必须把对变量的修改刷新回主内存。
  3. 有序性(Ordering)

    • 同步块的进入和退出建立了严格的 happens-before 关系:

      • 解锁(unlock)happens-before 随后对同一把锁的加锁(lock)
    • 确保临界区内代码不会因指令重排序而影响同步语义。

实现方式(基于 JMM 的 8 种原子操作):

  • lock:线程获得锁,清空工作内存对应变量。
  • unlock:线程释放锁,强制将修改后的变量值刷新到主内存。
  • 通过这两个操作,synchronized 确保了 互斥 + 内存可见性 + 有序性

👉 总结:

  • volatile:保证可见性 + 有序性,不保证复合操作的原子性。
  • synchronized:保证原子性 + 可见性 + 有序性,是更强的同步机制。

问题5

JMM 提出了 happens-before 原则 来帮助开发者理解并发语义。请列出至少三条 happens-before 规则,并解释它们的含义。


JMM 的 happens-before 原则 定义了哪些操作在并发环境下保证可见性和顺序性。常见规则包括:

  1. 程序次序规则

    • 在一个线程内,代码按程序顺序执行。
    • 即使编译器/CPU 有重排序优化,也不会改变单线程语义。
  2. 监视器锁定规则

    • 对一个锁的解锁(unlock)happens-before 随后对该锁的加锁(lock)。
    • 确保临界区内修改对后续持有同一锁的线程可见。
  3. volatile 变量规则

    • 对一个 volatile 变量的写操作 happens-before 后续对该变量的读操作。
    • 确保对 volatile 的修改能被立即看见。
  4. 线程启动规则

    • 在线程 A 启动线程 B(调用 Thread.start())之前,A 对共享变量的修改对 B 可见。
  5. 线程终止规则

    • 一个线程的所有操作 happens-before 另一个线程检测到该线程结束(通过 Thread.join()Thread.isAlive() 等)。
  6. 线程中断规则

    • 对线程调用 interrupt() happens-before 线程检测到中断事件(通过 interrupted()isInterrupted())。
  7. 对象终结规则

    • 对象的构造函数执行结束 happens-before 它的 finalize() 方法开始。

👉 总结:
happens-before 原则给出了“哪些操作的结果对其他线程可见”的契约。只要程序能建立 happens-before 关系,就能保证线程间的内存可见性和顺序性。


问题6

为什么说 JMM 是一种语言级别的内存模型?它与具体硬件的内存模型之间是什么关系?


  1. JMM 是语言级内存模型

    • JMM(Java Memory Model)是 Java 语言规范的一部分。
    • 它规定了 Java 程序在并发环境下的 可见性、有序性、原子性 语义。
    • 开发者写并发代码时只需遵守 JMM 的规则,而不用直接面对底层硬件的复杂性。
  2. 与硬件内存模型的关系

    • 不同 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++ 的例子说明。


  1. JMM 的保证范围

    • JMM 保证了 可见性(修改会及时写回主内存)和 有序性(通过 happens-before 规则禁止某些重排序)。
    • JMM 并不保证复合操作的原子性
  2. i++ 的拆解过程
    在字节码层面,i++ 通常分为三步:

    • load i:读取 i 的值到工作内存/寄存器。
    • add 1:在执行引擎中完成加法运算。
    • store i:将新值写回工作内存,再同步到主内存。

    如果有两个线程同时执行 i++,可能发生:

    • 线程 A 读取了 i = 10;
    • 线程 B 也读取了 i = 10;
    • 两个线程各自计算 i+1 = 11,并写回主内存;
    • 最终结果是 i = 11,而不是 12。
  3. 为什么 JMM 不保证原子性

    • 原子性保证需要更强的同步机制(如锁),会降低性能。
    • JMM 的设计目标是 最小化限制,只保证可见性和有序性,原子性交由程序员通过 synchronizedLockAtomicInteger 等工具保证。

👉 总结:

  • volatile 变量虽然能保证可见性,但不能保证 i++ 的原子性。
  • 要保证 i++ 的原子性,必须使用 synchronized 或 *原子类(Atomic)**。

问题9

请解释一下 Java 内存模型中的“先行发生(happens-before)”与“程序次序(as-if-serial)”的区别与联系


  1. as-if-serial 语义

    • 是单线程程序的保证:不论编译器和 CPU 如何优化,程序执行结果必须与按程序顺序执行的结果一致

    • 它允许重排序,但只要不改变单线程的最终结果,就被认为等价。

    • 举例:

      int a = 1;
      int b = 2;
      

      编译器可以先赋值 b,再赋值 a,因为最终结果一致。

  2. happens-before 关系

    • 是多线程环境下的保证:前一个操作的结果对后续操作可见,并且前一个操作的执行顺序排在后一个之前

    • 它定义了并发语义下的可见性与有序性约束(比如 volatile 写 → volatile 读;unlock → lock)。

    • 举例:

      • 线程 A 写 volatile 变量 → 线程 B 随后读该变量,A 的写对 B 可见。
  3. 两者联系

    • as-if-serial 是单线程的保证,确保优化不会改变程序逻辑结果。
    • happens-before 是跨线程的保证,确保并发程序的执行符合预期的可见性/顺序性。
    • 二者结合起来,构成了 Java 内存模型对单线程和多线程的完整语义约束。

👉 总结:

  • as-if-serial = 单线程结果一致性保证。
  • happens-before = 多线程间的可见性和顺序性保证。

问题10

在 JMM 中,final 字段 有一条特殊的语义。请问它的作用是什么?它是如何保证对象构造安全性的?

(提示:和 “安全发布” 有关,关键点是对象构造过程中的写 → 读顺序)


JMM 中,final 字段的特殊语义 是:

  1. 禁止重排序

    • 对 final 字段的写入操作,必须在构造函数结束前完成,不能与对象引用的赋值重排序。

    • 换句话说:

      class A {final int x;A() {x = 42;   // 写 final 字段}
      }
      

      JMM 保证 x=42 一定在对象引用“this”被发布之前完成。

  2. 安全发布

    • 一旦构造函数执行完毕,其他线程通过正确方式(如对象引用)访问这个对象时,一定能看到 final 字段的正确值,而不会读到默认值(0/null)。
    • 这保证了 final 字段的“初始化安全性”。
  3. 区别于普通字段

    • 普通字段的写入可能与对象引用赋值发生重排序,从而导致其他线程读到未初始化的值。
    • final 字段则通过 JMM 的额外规则避免了这种情况。

👉 总结:

  • 语法层面:final 修饰引用时,引用本身不可变,但对象内容可变。
  • JMM 层面:final 字段有 写屏障 语义,保证构造完成后被安全发布,其他线程一定能读到初始化后的值。

问题11

JMM 如何通过 内存屏障(Memory Barrier) 来落实到底层硬件?请举例说明读写屏障分别的作用。


在 JMM 中,内存屏障(Memory Barrier / Fence) 是实现 happens-before 语义的底层手段。

  1. 概念

    • 内存屏障是一类特殊的 CPU 指令,作用是:

      • 禁止特定的指令重排序
      • 刷新处理器缓存,保证数据在多核环境下可见。
    • JVM 在 volatile/synchronized 等地方插入屏障,来把 JMM 规则落实到不同 CPU 架构。

  2. 常见类型(在 JMM 的抽象里主要有 4 种,实际 CPU 实现略有不同):

    • LoadLoad 屏障:保证前面的读操作完成后,后续读操作才能执行。
    • StoreStore 屏障:保证前面的写操作对其他处理器可见后,后续写操作才能执行。
    • LoadStore 屏障:保证前面的读完成后,后续写才能执行。
    • StoreLoad 屏障:最强的屏障,保证前面的写对所有处理器可见后,后续读才能执行(通常会导致 CPU pipeline flush)。
  3. 应用示例

    • volatile 写操作:在写之前插入 StoreStore,在写之后插入 StoreLoad → 保证写的可见性 + 禁止后续读/写乱序。
    • volatile 读操作:在读之前插入 LoadLoad,在读之后插入 LoadStore → 保证读能获取到主内存最新值,并禁止与后续操作乱序。

👉 总结:

  • 内存屏障 = JVM 在底层 CPU 指令级别的“防护栅栏”。
  • 它保证了 JMM 的可见性和有序性 能在不同硬件(x86、ARM、PowerPC)上都生效。

问题12

在 JMM 的 happens-before 规则中,有三条是专门针对 线程操作 的。请分别说明:

  1. 线程启动规则
  2. 线程终止规则
  3. 线程中断规则

JMM 的 happens-before 规则里,关于线程有三条专门规定:

  1. 线程启动规则

    • 主线程调用 Thread.start() 之前,对共享变量的修改,
    • 对启动的子线程 是可见的
    • 保证子线程能看到父线程在启动前完成的初始化操作。
  2. 线程终止规则

    • 一个线程中的所有操作,
    • happens-before 另一个线程成功从 Thread.join() 返回,或检测到该线程已终止(如 isAlive()==false
    • 保证父线程能看到子线程执行完的结果。
  3. 线程中断规则

    • 调用 Thread.interrupt()
    • happens-before 被中断线程检测到中断事件(通过 interrupted()isInterrupted()
    • 保证中断信号不会丢失,线程能及时感知。

👉 总结:

  • start() → 子线程操作可见
  • 子线程操作 → join()/isAlive 可见
  • interrupt() → 中断检测可见

下一题(问题13)

除了线程规则外,JMM 还规定了一条 对象终结(finalize)规则。请问它的含义是什么?


在 JMM 中,关于 对象终结(finalize) 有一条 happens-before 规则:

  • 一个对象的构造函数执行结束
    happens-before 该对象的 finalize() 方法的开始

含义:

  1. 保证在 finalize() 方法里看到的对象状态,至少包含 构造函数完成时的结果
  2. 避免了对象在 finalize 中读到“未初始化字段”的情况。

👉 总结:

  • finalize 的 happens-before 规则确保了 对象构造 → 对象终结 之间的顺序性。

  • 不过需要注意:

    • finalize() 只会被 JVM 至少调用一次,但调用时机不确定。
    • 在现代 Java 开发中,finalize() 已被标记为 不推荐使用,替代方案是 try-with-resourcesCleaner

问题14

JMM 的规则不仅体现在语言层面,还影响了并发工具的实现。请举例说明 JMM 在 并发工具类(如 Lock、AQS、原子类) 中的体现。


  1. 原子类(AtomicXXX)

    • 基于 CAS(Compare-And-Swap) 实现无锁操作。
    • CAS 本质依赖 JMM 的 volatile 变量保证可见性,以及 CPU 的原子指令保证比较和更新的原子性。
    • 例如:AtomicInteger.incrementAndGet() 内部用 Unsafe.compareAndSwapInt()
  2. Lock 和 AQS(AbstractQueuedSynchronizer)

    • AQS 中的 state 字段 通常用 volatile 修饰,保证不同线程能立即看到锁状态的变化。

    • 加锁与解锁操作通过 happens-before 规则 建立可见性和有序性:

      • unlock() happens-before lock()。
    • 线程排队依赖 CAS 来竞争锁状态。

  3. 并发集合 / 阻塞队列

    • 内部大量使用 volatile + CAS 保证队列节点(指针/next)的安全修改。
    • 这就是你说的“双指针”思想:通过 CAS 更新 head/tail 指针,保证链表/队列并发安全。
  4. synchronized 的实现

    • JVM 层面直接依赖 JMM 的 lock/unlock 语义,确保原子性、可见性和有序性。

👉 总结:

  • CAS + volatile + happens-before 是 Java 并发工具的三大基石。
  • JMM 提供了 内存可见性和有序性保证,让高层并发工具能安全高效地运行。

问题15

请系统总结 第 12 章《Java 内存模型与线程》 的主要知识点:

  • JMM 抽象模型(主内存 / 工作内存)
  • 内存交互的 8 种操作
  • volatile 与 synchronized 的语义
  • happens-before 原则及其规则
  • 特殊语义(long/double、final 字段)
  • JMM 与硬件模型的关系
  • 并发工具的实现基础

  1. JMM 抽象模型

    • 主内存(Main Memory):所有线程共享,保存实例字段、静态字段、数组元素。
    • 工作内存(Working Memory):线程私有,存放主内存变量的副本,线程操作必须先拷贝到工作内存再执行。
    • 线程之间的通信必须经过主内存,工作内存不可直接共享。
  2. 内存交互的 8 种原子操作

    • lock / unlock(锁定、解锁)
    • read / load(从主内存读取 → 拷贝到工作内存)
    • use / assign(在工作内存使用/赋值变量)
    • store / write(把工作内存值写回主内存)
    • 这八种操作定义了线程与主内存交互的规则。
  3. volatile 的语义

    • 可见性:写入立即刷新主内存,读时强制从主内存获取。
    • 有序性:禁止 volatile 前后的指令重排序(通过内存屏障实现)。
    • 不保证原子性i++ 等复合操作仍然需要锁或原子类。
  4. synchronized 的语义

    • 原子性:临界区互斥访问。
    • 可见性:解锁前必须刷新主内存,加锁后必须清空工作内存。
    • 有序性unlock happens-before 后续的 lock
  5. happens-before 原则

    • 程序次序:单线程内代码按顺序执行。

    • 锁定规则:unlock → lock。

    • volatile 规则:写 → 读。

    • 线程规则

      • start() → 子线程操作
      • 子线程操作 → join()/isAlive()
      • interrupt() → 中断检测
    • 对象终结规则:构造结束 → finalize()。

  6. 特殊语义

    • long/double 非原子性协定:未加 volatile 的 64 位变量读写可能被拆分为两次 32 位操作。
    • final 字段语义:构造函数中对 final 字段的写入禁止与 this 引用重排序,保证对象发布安全。
  7. JMM 与硬件模型的关系

    • 不同硬件内存模型差异大(x86 较强,ARM/PowerPC 更弱)。
    • JVM 通过插入 内存屏障(Memory Barrier),将 JMM 的规则落实到底层 CPU 指令。
  8. 并发工具的实现基础

    • 原子类:依赖 CAS + volatile 保证原子性和可见性。
    • AQS / Lock:依赖 volatile 状态字段 + CAS 实现锁语义,happens-before 保证可见性。
    • 并发容器:广泛使用 volatile + CAS 更新节点指针(双指针算法)。


文章转载自:

http://YBuj8GRH.hwbmn.cn
http://yphMjQ6N.hwbmn.cn
http://2MKrtrEw.hwbmn.cn
http://q3FLsS8I.hwbmn.cn
http://E9AK2XaG.hwbmn.cn
http://JsWqH1Dk.hwbmn.cn
http://7B3VmsdT.hwbmn.cn
http://oGvU11RM.hwbmn.cn
http://BGGzFgwb.hwbmn.cn
http://thiFWZkG.hwbmn.cn
http://DH5oxcAI.hwbmn.cn
http://llHMpzGl.hwbmn.cn
http://d8GKKKgR.hwbmn.cn
http://vjjKeXkN.hwbmn.cn
http://pcUpBtqb.hwbmn.cn
http://hdmgBeIG.hwbmn.cn
http://YEW2bwkB.hwbmn.cn
http://DZP39eqK.hwbmn.cn
http://6u09rYRu.hwbmn.cn
http://sNwld953.hwbmn.cn
http://rsr7eLPo.hwbmn.cn
http://zOaWEy26.hwbmn.cn
http://IkLrwaih.hwbmn.cn
http://arYQAE83.hwbmn.cn
http://PjQQmDDY.hwbmn.cn
http://3CEJp8bD.hwbmn.cn
http://TZtZPw6X.hwbmn.cn
http://w0nhooZQ.hwbmn.cn
http://5Lm5JjCH.hwbmn.cn
http://t4PbYbjp.hwbmn.cn
http://www.dtcms.com/a/383963.html

相关文章:

  • 条件表达式和逻辑表达式
  • 《数据密集型应用系统设计2》--数据复制与数据分片
  • 【C++】揭秘:虚函数与多态的实现原理
  • 项目交付后知识沉淀断档怎么办
  • Spring事务传播行为全解析
  • OpenCV一些进阶操作
  • Layer、LayUI
  • 机器视觉光源的尺寸该如何选型的方法
  • MySQL 高阶查询语句详解:排序、分组、子查询与视图
  • Mathtype公式批量编号一键设置公式居中编号右对齐
  • CKS-CN 考试知识点分享(5) 安全上下文 Container Security Context
  • 简单的分数求和 区分double和float
  • Python核心技术开发指南(066)——封装
  • SFR-DeepResearch: 单智能体RL完胜复杂多智能体架构
  • 【Docker+Nginx+Ollama】前后端分离式项目部署(传统打包方式)
  • ffplay数据读取线程
  • 回溯剪枝的 “减法艺术”:化解超时危机的 “救命稻草”(二)
  • 16-21、从监督学习到深度学习的完整认知地图——机器学习核心知识体系总结
  • 二叉树的顺序存储
  • 第7课:本地服务MCP化改造
  • CF607B Zuma -提高+/省选-
  • DMA-API(map和unmap)调用流程分析(十一)
  • LeetCode 1898.可移除字符的最大数目
  • LeetCode算法日记 - Day 42: 岛屿数量、岛屿的最大面积
  • 局域网文件共享
  • llamafactory 部署教程
  • Linux链路聚合工具之ifenslave命令案例解析
  • 资金方视角下的链改2.0:拉菲资本的观察与判断
  • AIPex:AI + 自然语言驱动的浏览器自动化扩展
  • < JS事件循环系列【四】> 事件循环补充概念:从执行细节到性能优化