JVMNotes

第 09 章:Java 内存模型

zjc 于 2026-01-09 发布

这是《JVM 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开,代码块保留了原书可直接运行的版本。 Java 内存模型(JMM)定义多线程读写共享变量时的可见性、有序性和原子性。很多并发 bug 只在特定调度、缓存和编译器优化下出现,理解 JMM 才能判断代码是否正确。

9.1 问题来源

Thread A
  -> write shared variable
     -> store buffer / cache
        -> memory

Thread B
  -> read shared variable
     -> cache
        -> 可能读到旧值

编译器、CPU 和内存系统都可能重排指令。单线程语义不变时,重排对单线程不可见,但多线程之间可能产生不同结果。

三类问题:

问题 示例
可见性 一个线程修改,另一个线程看不到
原子性 count++ 非原子
有序性 语句编写顺序与观察顺序不同

9.2 happens-before

JMM 用 happens-before 表达可见性规则。若 A happens-before B,则 A 的结果对 B 可见。

常见规则:

  1. 单线程程序顺序规则;
  2. 监视器锁解锁 happens-before 后续锁定;
  3. volatile 写 happens-before 后续读;
  4. 线程 start happens-before 该线程内操作;
  5. 线程内操作 happens-before join 返回;
  6. 传递性;
  7. 线程中断调用 happens-before 检测中断;
  8. 对象默认初始化 happens-before 其他线程看到引用。

错误理解:happens-before 不是时间先后,而是内存可见性约束。

9.3 数据竞争

public class Counter {
    private int count;

    public void increment() {
        count++;
    }

    public int get() {
        return count;
    }
}

问题:

  1. 多线程写共享字段;
  2. 没有同步;
  3. count++ 分为读、加、写;
  4. 存在数据竞争。

正确方案:

private final AtomicInteger count = new AtomicInteger();

public void increment() {
    count.incrementAndGet();
}

或:

public synchronized void increment() {
    count++;
}

9.4 重排示例

public class ReorderDemo {
    int a = 0;
    boolean ready = false;

    public void writer() {
        a = 1;
        ready = true;
    }

    public void reader() {
        if (ready) {
            System.out.println(a);
        }
    }
}

多线程下 reader 可能观察到 ready == truea == 0

修复:

volatile boolean ready = false;

volatile 写之前的写,对后续 volatile 读可见,并限制相关重排。

9.5 final 语义

正确构造对象后,final 字段具有安全发布语义。

public class Config {
    private final int version;
    private final List<String> nodes;

    public Config(int version, List<String> nodes) {
        this.version = version;
        this.nodes = List.copyOf(nodes);
    }
}

注意:

  1. 构造函数中不要泄漏 this;
  2. final 引用不可变不代表对象内部可变状态线程安全;
  3. 集合应使用不可变集合;
  4. 静态工厂返回不可变对象更清晰。

9.6 安全发布

不安全:

public mutable Object instance;

其他线程可能看到部分构造对象。

安全方式:

方式 机制
volatile 字段 可见性和禁止重排
final 字段 正确构造后的语义
互斥和内存语义
并发容器 内部同步
Atomic 类 CAS 和 volatile
线程 start / join happens-before

9.7 原子性

操作 是否原子
引用赋值 通常原子,但可见性需另行保证
long / double 写 Java 规范允许特殊处理,volatile 可保证
i++
AtomicLong.incrementAndGet
ConcurrentHashMap.put 单个映射操作原子

ConcurrentHashMap 保证单个操作的原子性,复合操作仍需 compute 等原子方法:

map.compute(key, (k, old) -> old == null ? 1 : old + 1);

9.8 内存屏障

常见屏障:

屏障 作用
LoadLoad 前面读与后面读不重排
StoreStore 前面写与后面写不重排
LoadStore 前面读与后面写不重排
StoreLoad 前面写与后面读不重排

volatile 在不同 CPU 架构上可能生成不同指令。JMM 屏蔽硬件差异,编写代码时关注 Java 语义即可。

9.9 常见误区

误区 事实
volatile 让 ++ 原子 不会
synchronized 只做互斥 还有内存语义
sleep 会释放锁 不释放
双重检查不需要 volatile 需要正确写法
HashMap 加锁读就一定安全 也要看读写路径
ConcurrentHashMap 复合操作天然原子 不一定

双重检查锁:

public class Singleton {
    private static volatile Singleton instance;

    public static Singleton getInstance() {
        Singleton result = instance;
        if (result == null) {
            synchronized (Singleton.class) {
                result = instance;
                if (result == null) {
                    result = new Singleton();
                    instance = result;
                }
            }
        }
        return result;
    }
}

更简单的方式是使用静态 holder 或 enum。

9.10 JMM 与 GC

JMM 主要讨论线程可见性,GC 主要讨论对象生命周期,但它们在引用发布上存在交互:

  1. 安全发布让 GC 能一致地看到引用;
  2. weak / soft / phantom 引用读取需要同步;
  3. ReferenceQueue 清理与业务线程并发;
  4. finalizer 语义复杂,不推荐资源清理;
  5. Cleaner 也有时序限制。

堆外资源应使用 try-with-resources 明确释放。

本章小结

JMM 用 happens-before 定义多线程可见性。解决数据竞争要选择锁、volatile、Atomic、并发容器或不可变对象。volatile 提供可见性和有序性,不提供复合操作原子性。并发正确性先于性能优化。

思考题

  1. happens-before 和时间先后有什么区别?
  2. 为什么 count++ 不是原子操作?
  3. volatile 能解决哪些问题,不能解决哪些问题?
  4. final 的安全发布语义是什么?
  5. 如何证明一段代码存在数据竞争?