volatile 修饰的变量具有以下特性:
- 可见性。对一个 volatile 变量的读,总是能看到任意线程对这个 volatile 变量最后的写入。
- 原子性。对任意单个 volatile 变量的读/写具有原子性(除去 double 和 float 类型变量,因为这两种类型变量为 64 位类型)。
一个 volatile 变量的单个读/写操作,与锁对普通变量的读/写,它们之间的执行效果相同。
volatile 的 happens-before 规则:一个 volatile 的写 happens-before 于volatile 的读。
volatile 写–读建立的 happens-before 关系
class ReorderExample {
int a = 0;
boolean flag = false;
public void writer() {
a = 1; // 1
flag = true; // 2
}
public void reader() {
if(flag) { // 3
int i = a * a; // 4
}
}
}
假设线程 A 执行 writer() 方法之后,线程 B 执行 reader() 方法。根据 happnes-before 规则进行分析:
1)根据程序次序规则,1 happens-before 2;3 happens-before 4。
2)根据 volatile 规则,2 happens-before 3。
3)根据 happens-before 的传递性规则,1 happens-before 4。
volatile 的内存语义
当写一个 volatile 变量时,JMM 会把该线程对应的本地内存中的共享变量值刷新到主内存。
当读一个 volatile 变量时,JMM 会把该线程对应的本地内存设置为无效。
volatile 的线程通信理解:
volatile 变量修改后,因为 volatile 的读总能看到 volatile 的写,在对 volatile 修饰变量进行写入操作时,本地内存中的共享变量进行修改后直接刷新到主内存;此时进行 volatile 读时,本地内存直接无效化,需要重新从主内存中获取。即 volatile 的一次写读就是线程之间进行一次通信,保证数据正确。
volatile 内存语义的实现
在每个 volatile 写操作的前面插入一个 StoreStore 屏障。 写操作执行前必须保证上面数据对其他处理器可见。
在每个 volatile 写操作的后面插入一个 StoreLoad 屏障。写操作之后要保证数据刷新到主内存。
在每个 volatile 读操作的后面插入一个 LoadLoad 屏障。保证读操作比后续装载操作先装载数据,防止重排序导致数据执行错误。
在每个 volatile 读操作的后面插入一个 LoadStore 屏障。保证读操作比后续存储操作先装载数据,使后续存储操作获得到最新数据。
总结
读 volatile 操作通过将其他线程的本地内存无效化,强制从主内存中获取数据,达到数据实时更新效果。通过该方式,volatile 的写与读操作相当于线程间进行隐式通信。
引申一点,锁的内存语义与 volatile 一致,释放锁后数据必须对获取锁可见。