Java内存模型和volatile
2025-01-22 08:19:30    1.7k 字   
This post is also available in English and alternative languages.

当代多核(core)CPU中,通常是每个核(core)都有一级或两级高速缓存(cache),高速缓存较好解决了CPU与内存(RAM)的速度矛盾,但引入了新的问题,即:缓存一致性(Cache Coherence)。

同一数据的多个副本可能同时存在于不同核(core)的高速缓存(cache)中。若允许CPU自由地修改它们自己的副本,就会导致不同核(core)的高速缓存(cache)对存储器中同一数据的反映不一致。

建议先阅读此篇:计算机组成原理_CPU缓存 ,了解CPU缓存、MESI的概念为后续内容理解提供支撑。

操作系统层面的解决方案:

  1. 总线加锁 或者 cache line加锁
  2. 缓存一致性协议

1. 术语约定

  1. 线程工作内存 = 线程本地内存
  2. 主存 = Main Memory = 多个DRAM内存模块组成 = 内存条
  3. core = CPU的一个核心

2. Java Memory Model

Java定义了一种Java内存模型(Java Memory Model)来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果。

The Java Memory Model describes what behaviors are legal in multithreaded code, and how threads may interact through memory. It describes the relationship between variables in a program and the low-level details of storing and retrieving them to and from memory or registers in a real computer system. It does this in a way that can be implemented correctly using a wide variety of hardware and a wide variety of compiler optimizations. ---- 详阅 JSR 133 (Java Memory Model) FAQ

Java内存模型(Java Memory Model)控制了Java线程之间的通信,其采用共享内存通信机制,线程间的通信是隐式的,而同步是显示进行的,Java内存模型(Java Memory Model)决定了一个线程对共享变量的写入何时对另一个线程可见。

从抽象的角度看,Java内存模型(Java Memory Model)定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory),本地内存中存储了该线程读写共享变量的副本。

Java内存模型


3. Java Memory Model 数据原子操作

线程如何从主内存中获取数据,写入线程工作内存中?

线程操作、更新完数据,又是如何将线程工作内存中的数据回写到主内存中的?


3.1. 八个原子操作

Java Memory Model通过执行以下八个原子操作,确保数据在主内存与线程工作内存流转过程中的准确。

  • use - 使用
    从线程的工作内存读取数据用于计算。

A use action (by a thread) transfers the contents of the thread’s working copy of a variable to the thread’s execution engine. This action is performed whenever a thread executes a virtual machine instruction that uses the value of a variable.

  • assign - 赋值
    将线程计算好的值重新赋值到线程的工作内存中。

An assign action (by a thread) transfers a value from the thread’s execution engine into the thread’s working copy of a variable. This action is performed whenever a thread executes a virtual machine instruction that assigns to a variable.

  • read - 读取
    把从主内存读取的数据传输到线程的工作内存中,供后续load操作使用。

A read action (by the main memory) transmits the contents of the master copy of a variable to a thread’s working memory for use by a later load operation.

  • load - 载入
    read操作从主内存中读取到的数据放入线程的工作内存。

A load action (by a thread) puts a value transmitted from main memory by a read action into the thread’s working copy of a variable.

  • store - 存储
    把线程工作内存中的变量值传送到主内存中,供后续write操作使用。

A store action (by a thread) transmits the contents of the thread’s working copy of a variable to main memory for use by a later write operation.

  • write - 写入
    store操作从线程工作内存得到的变量值写入主内存中。

A write action (by the main memory) puts a value transmitted from the thread’s working memory by a store action into the master copy of a variable in main memory.

  • lock - 锁定
    由一个与主内存紧密同步的线程来完成(可以认为是由主内存执行),将主内存变量加锁,标识为线程独占状态。

A lock action (by a thread tightly synchronized with main memory) causes a thread to acquire one claim on a particular lock.

  • unlock - 解锁
    由一个与主内存紧密同步的线程来完成(可以认为是由主内存执行),将主内存变量解锁。

An unlock action (by a thread tightly synchronized with main memory) causes a thread to release one claim on a particular lock.


3.2. 操作流转示意图

结合下面的示例代码,理解原子操作。

  1. 主内存中 interruptFlag = false

  2. 程序启动,线程1开始运行,通过read操作从主内存中读取interruptFlag = false,保存到线程工作内存中。

  3. 通过load操作,把false赋值给工作内存中的interruptFlag。

  4. 线程1从工作内存读取数据,用于逻辑处理。

以上,线程1 和 线程2 都是一样的。

此时,线程1的逻辑在等待interruptFlag值做出改变,以中断程序;而线程2正准备更新interruptFlag值。


  1. 线程2将interruptFlag值改为true。

  2. 通过assign操作,将interruptFlag改变后的值传输到线程工作内存中。

  3. 通过store操作,将线程工作内存中interruptFlag改变后的值,传送到主内存中。

  4. 然后通过write操作,将 interruptFlag=true 写入主内存。

注意:此时线程2修改 interruptFlag = true,对于线程1还是不可见的,因为 interruptFlag 没有使用volatile关键字修饰。

数据原子操作流转


3.3. 示例代码

线程1将interruptFlag字段值更新,如果没有添加volatile关键字的,线程2是无法感知到数据变更的。

直观的体现了Java内存模型中的可见性问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class TestApp {
/**
* 中断标识
*/
//public static volatile Boolean interruptFlag = Boolean.FALSE;
public static Boolean interruptFlag = Boolean.FALSE;

public static void main(String[] args) throws Exception {
//线程2
new Thread(() -> {
while (!interruptFlag) {
System.out.println("等待中, " + System.currentTimeMillis());
}
System.out.println("运行结束, " + System.currentTimeMillis());
}).start();

TimeUnit.MILLISECONDS.sleep(2000);

//线程2
new Thread(() -> {
System.out.println("--正在准备数据-- ," + System.currentTimeMillis());
interruptFlag = Boolean.TRUE;
System.out.println("--数据准备结束-- ," + System.currentTimeMillis());
}).start();
}
}

4. Reference