# final 可见性
被 final 修饰的字段在声明时或者构造器中,一旦初始化完成(且无 this 引用逃逸),final 变量的值立刻回写到主内存,其他线程无须同步就能正确看见 final 字段的值
换句话说,即当一个对象包含 final 修饰的实例字段时,其他线程能够看到已经初始化的 final 实例字段
this 引用逃逸:指对象还没有构造完成,它的 this 引用就被发布出去了,其他线程有可能通过这个逸出的引用访问到“初始化了一半”的对象
# volatile 两种语义
Java 语言中,volatile 关键字主要具备 2 种语义,可见性和有序性
语义一:可见性
CPU 缓存会造成可见性问题,而 volatile 在 C 语言中,最原始的意义就是禁用 CPU 缓存
JMM 规定,volatile 变量每次修改都必须立刻回写主存,每次使用都需要从主存中刷新最新的值:
- 每次对变量的修改,都会引起 CPU 缓存(工作内存)写回到主存
- 一个工作内存回写到主存会导致其他线程的 CPU 缓存(工作内存)无效
语义二:有序性
Java 中,如果两个语句之间没有依赖关系,就可能被重排序优化,具体表现为:如果在本线程中观察,所有的操作都是有序的;如果在另一个线程中观察,所有的操作都是无序的volatile 关键字主要是通过禁止重排序来解决多线程程序的有序性问题,具体表现为:当程序执行到 volatile 变量的读或写时,在其前面的操作肯定全部已经执行完毕,且结果已经对后面的操作可见;在其后面的操作肯定还没有执行
# volatile 实现原理
# 内存屏障
内存屏障是插入两个 CPU 指令之间的一种指令,用来禁止 CPU 发生重排序保证有序性;且为了达到屏障效果,部分内存屏障会使得 CPU 写入/读取值之前,将缓冲区/缓存的数据刷新写回主内存,因此附带保证了可见性,因此可以总结,内存屏障有 2 个作用 :
- 阻止屏障两边的指令重排序
- 强制把写缓冲区/高速缓存中的脏数据等写回主内存,让其他 CPU 缓存中相应的数据失效
基本内存屏障:
主要有以下 4 类,LoadLoad、LoadStore、StoreStore 和 StoreLoad,这类屏障可统一用 XY 屏障来表示,即禁止屏障左侧的任何 X 操作与屏障右侧的任何 Y 操作之间重排序
屏障类型 | 说明 |
---|---|
Load1; LoadLoad; Load2 | 确保 Load1 数据加载,先于 Load2 及后续 Load 指令 |
Store1; StoreStore; Store2 | 确保 Store1 已经刷新到内存,先于 Store2 及后续 Store 指令 |
Load1; LoadStore; Store2 | 确保 Load1 数据加载,先于 Store2 及后续 Store 指令 |
Store1; StoreLoad; Load2 | 确保 Store1 已经刷新到内存, 先于 Load2 及后续 Load 指令 |
其中 StoreLoad 是万能屏障,兼具其他 3 个屏障的功能,会在写操作完之后,将写缓冲器中的条目冲刷入主内存;在读操作之前,清空无效化队列,从主内存或其他处理器的高速缓存中读取最新值到自己的内存
内存屏障分类:
按照可见性保障来分:加载屏障(LoadBarrier)和存储屏障(StoreBarrier)
- 加载屏障:StoreLoad 可以作为加载屏障,使得 CPU 在读共享变量前,先从主内存更新自己的缓存
- 存储屏障:StoreLoad 也可作为存储屏障,使得 CPU 在写共享变量后,将更新写回主内存
按照有序性保障来分:获取屏障(AcquireBarrier)和释放屏障(ReleaseBarrier)
- 获取屏障:LoadLoad 和 LoadStore 组合,在读操作后插入,禁止读操作与后续读写操作重排序
- 释放屏障:LoadStore 和 StoreStore 组合,在写操作前插入,禁止写操作与前面读写操作重排序
# 内存屏障实现 volatile
1. volatile 变量写
<img src="/img/Java/volatileW.png" style="zoom:70%">
2. volatile 变量读
<img src="/img/Java/volatileR.png" style="zoom:70%">
3. 内存屏障实现:Lock 前缀指令
非 volatile 变量
volatile 变量
# volatile 缺陷
volatile 关键字只保证可见性和有序性,不能保证原子性
对于读/写本身,Store 和 Load 指令是原子性的,但是对于复合操作如 i++,是由多个 CPU 指令完成的:
- 从主内存读取 i 到工作内存(load)
- 将 i 的值由工作内存压入操作栈进行计算(use)
- 计算完成,将结果由操作栈写回工作内存(assign)
- 将 i 的值写回主内存(store)
/**
* 尽管使用了 volatile 修饰变量,但是结果并不一定正确
* 假设线程 A,B 都进行了一次 counter++ 操作:
* 1. 线程 A,B 同时从主内存中读取值 0 写入工作内存开始计算
* 2. 线程 A 首先完成计算,将 counter 更新为 1,并对线程 B 可见
* 3. 线程 B 所在 CPU 嗅探到新值,将工作内存中值更新为 1,但此时 0 已经被压入操作栈
* 4. 线程 B 完成计算,值 1 从栈中弹出,写入工作内存,同时回写主内存
* 5. 最终两次 counter++ 操作结束后,counter 值为 1
*/
public class VolatileTest {
public static volatile int counter = 0;
public static void main(String[] args) throws Exception {
Thread pth1 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
counter++;
}
});
Thread pth2 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
counter++;
}
});
pth1.start();
pth2.start();
pth1.join();
pth2.join();
System.out.println(counter);
}
}
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
27
28
29
30
31
32
参考:
[1] volatile 如何保证可见性与有序性 (opens new window)
[2] 精确解释 java 的 volatile 之可见性、原子性、有序性(通过汇编语言) (opens new window)
[3] 【Java 并发学习三】 内存屏障与 synchronized、volatile 的原理 (opens new window)