深入浅出 Java 内存模型(JMM)与 volatile 关键字

发布于:2025-02-25 ⋅ 阅读:(10) ⋅ 点赞:(0)

深入浅出 Java 内存模型(JMM)与 volatile 关键字

在多线程编程中,Java 内存模型(Java Memory Model, JMM)规定了 线程 如何访问 共享变量,并提供了一组规则来保证数据一致性和线程安全。在理解 JMM 之前,我们需要先了解计算机的底层执行机制。


一、Java 内存模型(JMM)的核心概念

1. 为什么需要 JMM?

假设有两个线程 Thread AThread B,它们都访问一个共享变量 x

int x = 0;

Thread A: x = 10;  // 线程 A 修改变量 x
Thread B: System.out.println(x);  // 线程 B 读取变量 x

在单线程环境下,x 一定会被更新成 10,但在多线程环境下,Thread B 可能 看到的是 0,而不是 10。这是因为:

  • CPU 可能缓存变量 x 的旧值(可见性问题)。
  • CPU 可能对指令重新排序(有序性问题)。
  • 多个线程可能同时修改 x,导致数据损坏(原子性问题)。

2. JMM 主要解决的问题

Java 内存模型通过 原子性、有序性、可见性 这三个特性,确保多线程环境下的安全访问:

概念 定义 常见问题 如何保证
原子性(Atomicity) 操作不可被中断,要么完整执行,要么不执行 i++ 不是原子操作,多个线程同时执行可能导致数据错误 synchronized、Lock、Atomic 类
可见性(Visibility) 线程对变量的修改,其他线程能立即看到 线程 A 改变变量,但线程 B 读取到旧值 volatile、synchronized、Lock
有序性(Ordering) 代码的执行顺序与代码编写顺序一致 可能发生指令重排,导致预期之外的结果 volatile、synchronized、happens-before 原则

二、volatile 关键字的作用与原理

volatile 是 Java 提供的一种轻量级的同步机制,它主要用于 保证可见性和有序性,但不保证原子性

1. volatile 关键字的作用

volatile int x = 0;
  • 保证可见性:当一个线程修改 x,其他线程会立即看到最新值。
  • 防止指令重排:保证代码执行的顺序和编写顺序一致。

2. volatile 不保证原子性

假设有多个线程执行 x++ 操作:

volatile int x = 0;

public void increase() {
    x++;  // 实际上是 x = x + 1;
}

x++ 看似是一个简单的操作,但它其实是三步

  1. 读取 x 的值load
  2. 计算 x+1add
  3. 写入新值到 xstore

多个线程同时执行 x++ 可能会发生竞态条件,导致 x 计算错误:

Thread A: 读取 x = 5
Thread B: 读取 x = 5
Thread A: x = 5 + 1 = 6
Thread B: x = 5 + 1 = 6   // 预期是 7,但变成了 6!

如何解决?

  • synchronizedLock 保证 x++原子操作
  • AtomicInteger 替代 int
AtomicInteger x = new AtomicInteger(0);
x.incrementAndGet();  // 线程安全的自增

三、指令重排与 happens-before 原则

1. 什么是指令重排?

Java 代码的执行顺序 ≠ 代码的编写顺序,因为编译器和 CPU 可能会调整指令顺序以优化性能。例如:

int a = 10;
int b = 20;
int c = a + b;

可能会被重排成:

int b = 20;
int a = 10;
int c = a + b;

虽然在单线程环境下,指令重排不会影响最终结果,但在多线程环境下,它可能导致不可预测的问题。

2. happens-before 原则

为了保证线程间的正确性,Java 规定了 happens-before 关系,即如果操作 A happens-before 操作 B,那么 A 的结果一定对 B 可见。常见规则:

规则 示例
程序次序规则 单线程 中,前面的代码 happens-before 后面的代码
锁定规则 unlock() happens-before 之后的 lock()
volatile 规则 volatile 变量的写入 happens-before 之后的读取
线程启动规则 Thread.start() happens-before 线程中的代码
线程终止规则 线程中的代码 happens-before Thread.join()
传递性 A happens-before B,B happens-before C,则 A happens-before C

示例:

volatile int a = 0;
int b = 0;

public void writer() {
    a = 1;  // (1)
    b = 2;  // (2)
}

public void reader() {
    int x = b;  // (3)
    int y = a;  // (4)
}

由于 avolatile,保证:

  • (1) happens-before (4)
  • (2) 不一定 happens-before (3),导致 x=2y 可能还是 0

四、volatile 底层的内存屏障

为了保证 可见性和有序性volatile 会在 CPU 层面插入内存屏障(Memory Barrier),让不同 CPU 之间的数据保持一致。

  • 写入 volatile 变量 时,JVM 会在指令后插入 StoreStoreStoreLoad 屏障,确保所有写入操作不会被重排序到 volatile 变量之后。
  • 读取 volatile 变量 时,JVM 会插入 LoadLoadLoadStore 屏障,确保 volatile 变量读取前的所有读操作不会被重排序。

示例:

volatile boolean flag = false;

public void write() {
    flag = true; // StoreStore 屏障
}

public void read() {
    if (flag) { // LoadLoad 屏障
        System.out.println("可见性保证");
    }
}

五、总结

问题 解法
如何保证可见性? volatilesynchronizedLock
如何防止指令重排? volatilehappens-before 原则
如何保证原子性? synchronizedLockAtomic 变量
volatile 适用场景? 状态标志、双重检查锁(DCL)、轻量级同步

Java 内存模型是高性能并发编程的基础,理解 volatile、指令重排和 happens-before 原则能让我们写出更高效、安全的多线程代码!