Volatile
Volatile
volatile 作用
- 保证有序性:防止指令重排序
- 保证可见性:保证共享变量的可见性
为什么可以保证有序性(防止重排序)
为什么要重排序
- 图中左侧是 3 行 Java 代码,右侧是这 3 行代码可能被转化成的指令。
- 可以看出 a = 100 对应的是 Load a、Set to 100、Store a,意味着从主存中读取 a 的值,然后把值设置为 100,并存储回去,同理, b = 5 对应的是下面三行 Load b、Set to 5、Store b,最后的 a = a + 10,对应的是 Load a、Set to 110、Store a。
- 如果你仔细观察,会发现这里有两次“Load a”和两次“Store a”(图中黑线),说明存在一定的重排序的优化空间。
优化:
- 重排序后, a 的两次操作被放到一起,指令执行情况变为 Load a、Set to 100、Set to 110、 Store a。
- 下面和 b 相关的指令不变,仍对应 Load b、 Set to 5、Store b。
- 可以看出,重排序后 a 的相关指令发生了变化,节省了一次 Load a 和一次 Store a。
- 重排序通过减少执行指令,从而提高整体的运行速度,这就是重排序带来的优化和好处。
即指令重排序是在不影响单线程程序执行结果的前提下,计算机为了最大限度的发挥机器性能,会对机器指令重排序优化
但重排序会遵循 as-if-serial
与 happens-before
原则;
重排序带来的问题
单线程下,重排序时是没有问题的,但是多线程下就会出现问题
例如
1 |
|
会出现结果:
1 |
|
其中,最后一个就是指令重排序的结果,即将 2 排在了 1 前面,4 排在了 3前面。
怎么防止重排序
对于 volatile 修饰的变量,会使用内存屏障阻止重排序,内存屏障使用了 lock 前缀的汇编指令,该条指令的作用是通过硬件阻止重排序(见 lock 汇编指令第一条)
Java 规范定义的内存屏障
屏障类型 | 指令示例 | 说明 |
---|---|---|
LoadLoad | Load1;LoadLoad;Load2 | 保证 Load1 的读取操作在 Load2 及后续读取操作之前执行 |
StoreStore | Store1;StoreStore;Store2 | 在 Store2 及其后的写操作执行前,保证 Store1 的写操作已刷新到主内存 |
LoadStore | Load1;LoadStore;Store2 | 在 Store2 及其后的写操作执行前,保证 Load1 的读操作已读取结束 |
StoreLoad | Store1;StoreLoad;Load2 | 保证 Load1 的写操作已刷新到主内存之后,Load2 及其后的读操作才能执行 |
Load:读取
Store:写入
Java 规定 volatile 需要实现的内存屏障
- 写
- 在每个 volatile 写操作的前面插入一个 StoreStore 屏障。
- 在每个 volatile 写操作的后面插入一个 StoreLoad 屏障。
- 读
- 在每个 volatile 读操作的后面插入一个 LoadLoad 屏障。
- 在每个 volatile 读操作的后面插入一个 LoadStore 屏障
为什么可以保证可见性
对于 volatile 修饰的变量,会增加一条 lock 前缀的汇编指令,该条指令的作用是让当前线程的工作内存写入主存(见 lock 汇编指令第三条)
为什么不能保证原子性
1 |
|
对应的字节码如下:
1 |
|
参考《深入理解 Java 虚拟机》:i ++ 对应的操作对应绿色部分,当 getstatic 指令把 i 的值取到操作栈顶时,volatile 关键字保证了 i 的值在此时是正确的,但在执行 iconst_1, iadd 这些指令的时候,其他线程可能已经把 i 的值改变了,而操作栈顶的值就变成了过期的数据,所以 putstatic 指令执行后就可能把较小的 i 值同步回主存之中。
参考《为什么volatile能保证有序性不能保证原子性》:i ++,实际上是 3 步操作,首先读取 i 的值,然后将 i + 1 赋值给中间变量,最后将中间变量的值赋给 i;因此即使是使用 volatile 修饰,也只能保证第一步强制从主存获取 i 的值,假设两个线程(A和B)都已经获取到了 i 的值,也将 i + 1 的值赋给了中间变量,即使线程 A 修改完成后,并通知其他线程 i 的值已经失效,需要重新从主存中获取,但是线程 B 已经走到了第二步,i 的值更改成什么,都已经晚了,换句话说,volatile 确实是保证了可见性,但这个可见性来的太晚了一些;
保证原子性需要用到锁机制;
as-if-serial
as-if-serial 语义的意思是:不管怎么重排序(编译器和处理器为了提高并行度),(单线程)程序的执行结果不能被改变。编译器、runtime 和处理器都必须遵守 as-if-serial 语义。
为了遵守 as-if-serial 语义,编译器和处理器不会对存在数据依赖关系的操作做重排序,因为这种重排序会改变执行结果。但是,如果操作之间不存在数据依赖关系,这些操作就可能被编译器和处理器重排序。
happens-before
Java 内存模型具备一些先天的“有序性”,即不需要通过任何同步手段(volatile、synchronized 等)就能够得到保证的安全,这个通常也称为 happens-before
原则,它是可见性与有序性的一套规则总结
不符合 happens-before
规则,JMM
并不能保证一个线程的可见性和有序性
程序次序规则 (
Program Order Rule
):一个线程内,逻辑上书写在前面的操作先行发生于书写在后面的操作 ,因为多个操作之间有先后依赖关系,则不允许对这些操作进行重排序锁定规则 (
Monitor Lock Rule
):一个 unlock 操作先行发生于后面(时间的先后)对同一个锁的 lock 操作,所以线程解锁 m 之前对变量的写(解锁前会刷新到主内存中),对于接下来对 m 加锁的其它线程对该变量的读可见volatile 变量规则 (
Volatile Variable Rule
):对 volatile 变量的写操作先行发生于后面对这个变量的读传递规则 (
Transitivity
):具有传递性,如果操作 A 先行发生于操作 B,而操作 B 又先行发生于操作 C,则可以得出操作 A 先行发生于操作 C线程启动规则 (
Thread Start Rule
):Thread 对象的start()
方法先行发生于此线程中的每一个操作1
2static int x = 10;//线程 start 前对变量的写,对该线程开始后对该变量的读可见
new Thread(()->{ System.out.println(x); },"t1").start();线程中断规则 (
Thread Interruption Rule
):对线程interrupt()
方法的调用先行发生于被中断线程的代码检测到中断事件的发生线程终止规则 (
Thread Termination Rule
):线程中所有的操作都先行发生于线程的终止检测,可以通过Thread.join()
方法结束、Thread.isAlive()
的返回值手段检测到线程已经终止执行对象终结规则(
Finaizer Rule
):一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()
方法的开始
对比于 synchronized
volatile 只能解决可见性和有序性;
synchronized 可以解决原子性、可见性和有序性;
- 原子性:指的是一个或多个操作执行过程中不被打断的特性。被 synchronized 修饰的代码是具有原子性的,要么全部都能执行成功,要么都不成功。
- 可见性:指的是一个线程改变了共享变量之后,其他线程能够立即知道这个变量被修改。我们知道在 Java 内存模型中,不同线程拥有自己的本地内存,而本地内存是主内存的副本。如果线程修改了本地内存而没有去更新主内存,那么就无法保证可见性。synchronized在修改了本地内存中的变量后,解锁前会将本地内存修改的内容刷新到主内存中,确保了共享变量的值是最新的,也就保证了可见性。
- 有序性:指的是程序按照代码先后顺序执行。synchronized是能够保证有序性的。根据 as-if-serial 语义,无论编译器和处理器怎么优化或指令重排,单线程下的运行结果一定是正确的。而synchronized保证了单线程独占CPU,也就保证了有序性。
lock 汇编指令
不同的底层硬件对于 lock 指令是不同的,对于 IA-32 和 Intel 64 架构软件开发者手册中的解释是
会将当前处理器缓存行的数据立即写回到系统内存
这个写回内存的操作会引起在其他 CPU 里缓存了该内存地址的数据无效( MESI 协议),关于 MESI 协议,参阅:CPU缓存一致性协议MESI 。
提供内存屏障功能,使 lock 前后指令不能重排序(另一个作用,防止指令重排序)
Lock 前缀,Lock 不是一种内存屏障,但是它能完成类似内存屏障的功能。Lock 会对 CPU 总线和高速缓存加锁,可以理解为 CPU 指令级的一种锁。
JDK 源码实现
当变量名有 volatile 修饰时,赋值结束之后就会加上 storeload
storeload
实现就是加上 lock 前缀的汇编指令
图一的类和图二的源码如下
参考文章
- The JSR-133 Cookbook (oswego.edu)
- 什么是指令重排序?为什么要重排序?_HCH996的博客-CSDN博客
- 字节面试官:synchronized能保证可见性吗_sychorinized 能保证可见性吗_SKY技术修炼指南的博客-CSDN博客
- volatile禁止重排序的原理-内存屏障_volitile禁止指令重排的原理_bingaPang的博客-CSDN博客
- java并发编程(二)-volatile写操作前为什么不加LoadStore屏障_大臭太臭的博客-CSDN博客
- https://www.bilibili.com/video/BV1fD4y1e79C/?p=13