Java 内存模型(Java Memory Model,简称 JMM)描述了Java 程序中多线程如何共享数据以及如何同步的规则和规范。JMM 确定了 Java 虚拟机(JVM)如何在不同的硬件平台和操作系统上管理内存的一致性问题,从而保证 Java 的并发程序能够正确执行。
JMM 规定了变量(尤其是共享变量)在多个线程之间的可见性,并解决了重排序和缓存一致性的问题。
为什么需要 Java 内存模型?
在多线程编程中,多个线程共享同一块内存区域中的数据。当一个线程修改了共享变量的值,其他线程是否能立即看到这个修改?如何保证各个线程看到的变量值是一致的?JMM 主要为了解决这些问题。
现代计算机系统为了提高性能,常常会在硬件和编译器层面进行优化,这些优化带来了以下两个问题:
-
可见性问题:多个线程之间修改共享变量的结果可能不会立即在其他线程中可见。原因是 CPU 有自己的缓存,线程可能会在缓存中进行操作,而不是直接在主内存中读写。
-
指令重排序问题:为了提高性能,编译器和处理器可能会对指令进行重排序,这种重排序可能导致并发程序的执行顺序与开发者预期的不一致。
Java 内存模型通过以下方式保证了线程之间共享变量的可见性和有序性:
- Happens-before 规则:它定义了在多线程环境下,内存操作的先后顺序。
- volatile 关键字:保证可见性和一定的有序性。
- 锁机制(synchronized 和 ReentrantLock):保证临界区代码的原子性和有序性。
JMM 的三大特性
- 可见性(Visibility)
- 有序性(Ordering)
- 原子性(Atomicity)
1. 可见性(Visibility)
可见性指的是:当一个线程修改了共享变量的值,其他线程能够立即得知这个修改。
由于每个线程可能有自己的本地缓存,线程可能不会立刻将变量的修改刷新到主内存中,其他线程也可能不会立即从主内存中读取最新的值。JMM 通过以下机制来保证可见性:
-
volatile 关键字:当一个变量被声明为
volatile
时,JVM 保证对该变量的读写操作会直接在主内存中进行,任何线程对volatile
变量的修改都会立即对其他线程可见。 -
锁机制(synchronized, Lock):当一个线程获得锁时,JMM 保证该线程看到的是共享变量的最新值。线程释放锁之前,修改的变量会刷新到主内存中。
示例:
class Example {
private volatile boolean flag = true;
public void stop() {
flag = false;
}
public void doWork() {
while (flag) {
// do something
}
}
}
在上面的例子中,flag
变量是 volatile
类型,当一个线程调用 stop()
方法将 flag
设置为 false
后,其他线程在 doWork()
中会立即看到 flag
的变化。
2. 有序性(Ordering)
有序性指的是:程序的执行顺序看上去和代码的顺序一致。为了优化性能,编译器和处理器常常会对指令进行重排序,但 JMM 通过一些机制来约束这种重排序。
JMM 提供了两种保证有序性的手段:
-
happens-before 规则:这是一组规则,用于约定两个操作的执行顺序。可以认为,如果操作 A 先发生于操作 B(即 A happens-before B),那么 A 的结果对于 B 是可见的,且 A 的操作顺序不会被重排序到 B 之后。
-
volatile 关键字:
volatile
变量除了保证可见性外,还可以防止指令重排序。对volatile
变量的写操作之前的代码不会被编译器重排序到写操作之后,对volatile
变量的读操作之后的代码不会被重排序到读操作之前。
3. 原子性(Atomicity)
原子性指的是:一个操作要么全部执行完毕且不会被打断,要么就不执行。JMM 对以下操作提供了原子性保证:
-
基本读取和赋值操作:例如读取
int
、long
类型的变量(64位 JVM 中,long
和double
可能需要特别处理)是原子操作。 -
锁机制:如
synchronized
和ReentrantLock
保证对一个临界区的代码执行具备原子性。
需要注意的是,复合操作(如 i++
)不是原子操作。i++
实际上包含了三个步骤:读取 i
的值,i+1
,将 i
写回,这一过程中可能被其他线程打断。为了保证复合操作的原子性,通常需要使用同步机制:
class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
public synchronized int getCount() {
return count;
}
}
happens-before 规则
happens-before
规则是 JMM 中的一个核心概念,用于定义多个线程间的内存操作顺序,确保线程之间共享数据的一致性。JMM 中规定了一系列的 happens-before
规则,常见的有:
-
程序顺序规则:同一个线程中的每个操作,按照代码顺序,前面的操作
happens-before
后面的操作。 -
监视器锁规则:对于一个锁的释放操作,
happens-before
随后对该锁的获取操作。即一个线程释放了锁,另外一个线程获取了锁,那么这个线程可以看到前一个线程在释放锁之前对共享变量的所有修改。 -
volatile 变量规则:对一个
volatile
变量的写操作happens-before
读操作。一个线程写入了一个volatile
变量,另外一个线程可以立即看到这个变量的修改。 -
传递性:如果操作 A
happens-before
操作 B,且操作 Bhappens-before
操作 C,那么操作 Ahappens-before
操作 C。 -
线程启动规则:
Thread.start()
方法happens-before
该线程中任何操作。 -
线程中断规则:
Thread.interrupt()
方法happens-before
检测到该线程中断事件的代码(Thread.interrupted()
)。 -
线程终止规则:线程中的所有操作
happens-before
其他线程检测到该线程终止(如Thread.join()
)。
JMM 与 volatile
volatile
变量是 Java 内存模型中的一个关键字,用来保证共享变量的可见性和有序性,但不保证原子性。
volatile
的作用
-
可见性:当一个线程修改了
volatile
变量的值,其他线程能够立即看到这个修改。Java 内存模型保证任何线程在读取volatile
变量时,都会看到最新的值。 -
有序性:禁止指令重排序。对于
volatile
变量的写操作,写之前的操作不会被重排序到写操作之后,读操作之后的操作也不会被重排序到读操作之前。
class VolatileExample {
private volatile boolean flag = false;
public void writer() {
flag = true; // 写操作
}
public void reader() {
if (flag) { // 读操作
// 进行一些操作
}
}
}
在上面的例子中,flag
变量被声明为 volatile
,因此对 flag
的写操作会立即对其他线程可见,且 reader
中的判断 if (flag)
看到的值一定是最新的。