(17)《JVM 内存模型(JMM)》

2026-03-21
2
-
- 分钟
|

核心目标:解决多线程下共享数据的「可见性」「有序性」「原子性」问题。
关键原则:JMM 是 Java 规定的「规则手册」,不是实际内存结构。它确保线程间能正确看到数据变化,但不保证代码一定按顺序执行。


1. 原子性(Atomicity)

问题count++ 不是原子操作(底层有 读-改-写 3 步指令)。
解决方案synchronizedAtomicInteger
通俗解释
想象 synchronized(obj) 就像锁住一个 房间obj 是门锁)。

  • 线程 A 进门后反锁门 → 不能被其他线程打扰(保证 count++ 完整执行)。
  • 线程 B 想进门 → 发现门锁着 → 只能等。
    关键:必须锁 同一个对象
Object lock = new Object(); // 必须用同一个 lock
new Thread(() -> {
    synchronized(lock) { count++; } // 原子操作
}).start();

为什么不用 synchronized 频繁?
它是“重量级锁”(类似用铁链锁门),性能低。适合关键代码段。


2. 可见性(Visibility)

问题:线程 A 修改了 run = false,线程 B 还以为 run = true(因 CPU 缓存优化)。
死循环示例

boolean run = true;
new Thread(() -> {
    while (run) { /* 死循环 */ } 
}).start();
run = false; // 主线程改了值,但线程 B 看不到

为什么?
JVM 把 run 存在 本地缓存(CPU 仓库),线程 B 直接从仓库拿值,不看主内存。
解决方案volatile

volatile boolean run = true; // 每次读都去主内存拿!

通俗解释
volatile直接去仓库查货(不走本地缓存),保证修改对所有线程“可见”。
注意volatile 不保证原子性!(如 count++ 仍需 synchronized


3. 有序性(Ordering)

问题:JVM 优化时可能乱序执行指令(“指令重排”),导致多线程出错。
经典案例:Double-Checked Locking 单例

public class Singleton {
   private static volatile Singleton instance; // 必须加 volatile
   public static Singleton getInstance() {
       if (instance == null) {
           synchronized (Singleton.class) {
               if (instance == null) {
                   instance = new Singleton(); // 这里可能重排!
               }
           }
       }
       return instance;
   }
}

为什么重排会出错?
instance = new Singleton() 底层分三步:

  1. 分配内存
  2. 初始化对象
  3. 赋值给 instance
    重排风险:若 JVM 先执行步骤 3(赋值),再执行步骤 2(初始化),其他线程可能拿到 未初始化 的对象!
    解决方案volatile 禁止重排。
    通俗解释
    volatile给指令加“顺序标”,保证“先初始化,再赋值”。

4. happens-before(规则手册)

核心:定义“写操作何时对读操作可见”。
代码示例 + 解释(每种情况 1 行代码):

情况代码示例简单解释
1. 解锁后可见java<br>Object lock = new Object();<br>int x = 0;<br>synchronized(lock) { x = 42; } // 线程A写<br>synchronized(lock) { System.out.println(x); } // 线程B读(保证看到42)线程A 解锁 lock 后,x=42 对线程B 加锁后 的读操作可见。
2. volatile 写后可见java<br>volatile boolean flag = false;<br>flag = true; // 线程A写<br>while (!flag) {} // 线程B读(保证看到true)volatile 保证写操作 立即对其他线程可见
3. start 前写可见java<br>int data = 42;<br>Thread t = new Thread(() -> System.out.println(data));<br>t.start(); // start前data=42t.start() 的写(data=42),对线程 t 的读 可见
4. 结束前写可见java<br>int result = 0;<br>Thread t = new Thread(() -> result = 42);<br>t.start();<br>t.join(); // 等待t结束<br>System.out.println(result); // 保证看到42线程 t 结束前 的写(result=42),对 join() 后的读 可见
5. 打断前写可见java<br>int status = 0;<br>Thread t = new Thread(() -> { while (true) if (status == 1) break; });<br>t.start();<br>status = 1; // 打断前写<br>t.interrupt(); // 打断tstatus=1 t.interrupt() 前写,对 tbreak 可见
6. 默认值写可见java<br>int a = 0; // 默认值写<br>int b = a; // b=0(保证a的值可见)变量初始化(如 a=0)对后续变量(如 b=a可见
传递性java<br>// A happens-before B, B happens-before C → A happens-before C<br>volatile int x = 0; // A<br>x = 1; // B<br>System.out.println(x); // C(保证看到1)规则可叠加(如 A→BB→C,则 A→C)。

为什么 JMM 重要?

  • 不用它:多线程代码像“多人抢手机”——数据乱、死循环、空指针。
  • 用它synchronized/volatile 是 JMM 的“工具”,按规则用就安全。
    终极口诀

    volatile 保可见,synchronized 保原子,
    happens-before 是规则,乱序重排莫乱来!

评论交流

文章目录