JAVA内存模型

63 阅读6分钟

什么是 Java 内存模型(JMM)?

Java 内存模型(Java Memory Model, JMM)是一个抽象的概念,它描述了 Java 程序中各种变量(线程共享变量)的访问规则,以及在 JVM 中将变量存储到内存和从内存中读取变量这样的底层细节。

它的核心目标是解决在多线程环境下,由于存在本地缓存、指令重排序等问题导致的可见性、原子性和有序性问题,从而为开发者提供一套可靠的并发编程保证。

为了更好地理解,我们首先要区分 JMM 和 JVM 内存结构(如堆、栈、方法区):

  • JVM 内存结构:描述的是 运行时数据区 的划分,是 JVM 在物理上(或者说是逻辑上)如何管理内存的。它关心的是“数据存哪里”。
  • Java 内存模型:描述的是 线程和主内存之间的抽象关系,是一组规则和规范。它关心的是“在多线程环境下,如何正确地访问共享变量”。

JMM 的作用(为什么要存在 JMM?)

JMM 主要为了解决三个核心问题,这也是并发编程中的“三恶”:

  1. 可见性(Visibility)

    • 问题:一个线程修改了共享变量的值,其他线程能否立即看到修改后的值。
    • 根源:现代CPU为了性能,每个线程都有自己的工作内存(可以理解为CPU高速缓存),线程操作数据首先是在自己的工作内存中进行的,之后才刷新到主内存。这就可能导致一个线程的修改不会立刻被其他线程看到。
    • JMM 的作用:通过一套规则(如 volatilesynchronized 等关键字),保证一个线程对共享变量的写操作能可见于其他线程的后续读操作。
  2. 原子性(Atomicity)

    • 问题:一个或多个操作要么全部执行成功,要么全部不执行,中间不能被任何因素打断。
    • 根源:即使是简单的 i++ 操作,在底层也是由“读取、计算、写入”多个步骤完成的。在多线程环境下,这些步骤可能会被交错执行,导致结果不符合预期。
    • JMM 的作用:通过 synchronized 关键字或 Lock 接口来保证大块代码的原子性,并通过 java.util.concurrent.atomic 包下的原子类(如 AtomicInteger)来保证单一变量的简单操作的原子性。
  3. 有序性(Ordering)

    • 问题:程序执行的顺序不一定就是代码编写的顺序。
    • 根源:为了优化性能,编译器和处理器常常会对指令进行重排序。在单线程环境下,重排序不会影响最终结果(遵循 as-if-serial 语义)。但在多线程环境下,重排序可能会导致意想不到的问题。
    • JMM 的作用:通过 volatilesynchronized 等关键字建立 “happens-before” 规则,禁止特定类型的编译器重排序和处理器重排序,从而保证有序性。

JMM 的实现途径(如何保证可见性、有序性、原子性?)

JMM 定义了一系列的规则,而具体的实现则是通过使用特定的关键字和类库。

1. 主内存与工作内存的交互协议

JMM 定义了8种原子操作来完成主内存与工作内存之间的交互,如 lock(锁定)、unlock(解锁)、read(读取)、load(载入)、use(使用)、assign(赋值)、store(存储)、write(写入)。这些操作本身是原子的,但组合起来不一定原子。synchronized 就是通过这些底层操作来实现的。

2. 关键字和类库

  • synchronized 关键字

    • 原子性:通过 monitorentermonitorexit 指令,确保同步代码块在同一时刻只有一个线程能执行。
    • 可见性:JMM 规定,在释放锁之前,必须将工作内存中的变量刷新到主内存;在获取锁之后,必须清空工作内存,从主内存重新加载变量。
    • 有序性synchronized 同步块内的代码可以重排序,但不会逃逸到同步块之外(遵循管程锁定规则)。它保证了只有一个线程能进入临界区,所以内部的重排序不会影响最终结果。
  • volatile 关键字

    • 可见性:当一个变量被声明为 volatile 后,任何线程对它进行写操作,都会立即刷新到主内存。任何线程对它进行读操作,都会从主内存中重新读取最新值。
    • 有序性:通过添加内存屏障(Memory Barrier) 来禁止指令重排序。
      • volatile 变量时,屏障确保之前的操作都已完成且结果对后续操作可见。
      • volatile 变量时,屏障确保后续的所有读/写操作都在该读操作之后进行。
    • 注意volatile 不保证原子性(例如 volatile int i; i++; 不是原子操作)。
  • final 关键字

    • 只要对象是正确构造的(没有 this 引用逸出),被 final 修饰的字段在初始化后,其值就能保证对其他线程立即可见。
  • java.util.concurrent

    • 原子类(AtomicInteger, AtomicReference 等):通过 CAS(Compare-And-Swap)操作和 volatile 值相结合,提供了高效的原子操作,无需加锁。
    • 锁(Lock 接口,如 ReentrantLock:提供了比 synchronized 更灵活的锁操作,同样能保证原子性、可见性和有序性。
    • 并发容器(ConcurrentHashMap, CopyOnWriteArrayList 等):内部使用各种技巧(如分段锁、写时复制)来实现线程安全和高性能。
    • 注意: 并发包下的类都能保证线程安全,但是并不是所有类都能保证内存可见性,能实现内存可见性的类底层都是通过volatile(如atomic包下的原子类)和synchronized(如ReentrantLock)保证的。

3. Happens-Before 原则

这是 JMM 最核心的理论基础,是一组判断数据是否存在竞争、线程是否安全的规则。它无需任何同步手段辅助,是 JMM 提供给程序员的内存可见性保证。如果操作 A happens-before 于操作 B,那么 A 的结果对 B 可见。

常见的 happens-before 规则包括:

  • 程序次序规则:一个线程内,前面的操作 happens-before 于后续的任何操作。
  • 监视器锁规则:对一个锁的解锁 happens-before 于随后对这个锁的加锁。
  • volatile变量规则:对一个 volatile 变量的写操作 happens-before 于后续对这个变量的读操作。
  • 传递性:如果 A happens-before B,且 B happens-before C,那么 A happens-before C。

总结

特性问题描述JMM 实现途径
可见性一个线程的修改,其他线程看不到synchronized, volatile, final
原子性操作被中途打断,结果不完整synchronized, Lock, 原子类(AtomicXXX
有序性代码执行顺序与预期不符synchronized, volatile, happens-before 规则

Java 内存模型(JMM)是一套规范,它通过定义线程和主内存之间的抽象关系,以及提供 happens-before 规则,屏蔽了底层硬件(CPU缓存、指令重排序)的差异。Java 开发者通过使用 synchronizedvolatile 等关键字和 java.util.concurrent 工具包来遵循这套规范,从而编写出正确、高效的多线程程序。简单来说,JMM 是规则,而 synchronized 和 volatile 是实现这些规则的工具