volatile(2)
概述 目录
1.指令重排序的类型
2.volatile内存语义
3.volatile原理
第1节 指令重排序的类型
在执行程序时为了提高性能,编译器和处理器常常会对指令做重排序。重排序分三种类型:
(1)编译器优化的重排序
编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
(2)指令级并行的重排序
现代处理器采用了指令级并行技术(Instruction-Level Parallelism, ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
(3)内存系统的重排序
由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。
从Java最终实际执行的指令序列,会分别经历上面三种重排序。
上述的(1)属于编译器重排序,(2)和(2)属于处理器重排序。这些重排序都可能会导致多线程程序出现内存可见性问题。
对于编译器,JMM的编译器重排序规则会禁止特定类型的编译器重排序(不是所有的编译器重排序都要禁止)。
对于处理器重排序,JMM的处理器重排序规则会要求java编译器在生成指令序列时,插入特定类型的内存屏障(memory barriers,intel称之为memory fence)指令,通过内存屏障指令来禁止特定类型的处理器重排序(不是所有的处理器重排序都要禁止)。
JMM属于语言级的内存模型,它确保在不同的编译器和不同的处理器平台之上,通过禁止特定类型的编译器重排序和处理器重排序,为程序员提供一致的内存可见性保证。
public class Test {
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
}
}
}
如上图所示,操作1和操作2做了重排序。程序执行时,线程A首先写标记变量flag,随后线程B读这个变量。由于条件判断为真,线程B将读取变量a。此时,变量a还根本没有被线程A写入,在这里多线程程序的语义被重排序破坏了!
在程序中,操作3和操作4存在控制依赖关系。当代码中存在控制依赖性时,会影响指令序列执行的并行度。为此,编译器和处理器会采用猜测(Speculation)执行来克服控制相关性对并行度的影响。以处理器的猜测执行为例,执行线程B的处理器可以提前读取并计算a*a,然后把计算结果临时保存到一个名为重排序缓冲(reorder buffer ROB)的硬件缓存中。当接下来操作3的条件判断为真时,就把该计算结果写入变量i中。
从图中我们可以看出,猜测执行实质上对操作3和4做了重排序。重排序在这里破坏了多线程程序的语义!在单线程程序中,对存在控制依赖的操作重排序,不会改变执行结果(这也是as-if-serial语义允许对存在控制依赖的操作做重排序的原因);但在多线程程序中,对存在控制依赖的操作重排序,可能会改变程序的执行结果。
第2节 volatile内存语义
volatile可以保证线程可见性且提供了一定的有序性,但是无法保证原子性。在JVM底层volatile是采用“内存屏障”来实现的。
上面那段话,有两层语义
1.保证可见性、不保证原子性
2.禁止指令重排序
(1)可见性
可见性问题主要指一个线程修改了共享变量值,而另一个线程却看不到。引起可见性问题的主要原因是每个线程拥有自己的工作内存。volatile关键字能有效的解决这个问题,我们看下下面的例子,就可以知道其作用
Java中的happen-before规则,JSR 133中对Happen-before的定义如下:
· 程序顺序规则:一个线程中的每个操作,happens-before 于该线程中的任意后续操作。
· 监视器锁规则:对一个监视器的解锁,happens-before于随后对这个监视器的加锁。
· volatile 变量规则:对一个 volatile 域的写,happens-before 任意后续对 个 volatile 域的读。
· 传递性:如果 A happens-before B,且 B happens-before C,那么 A happens-before C。
· 线程的start() 方法 happen-before 该线程所有的后续操作
· 线程所有的操作 happen-before 其他线程在该线程上调用 join 返回成功后的操作
第3节 volatile原理
为了实现volatile可见性和happen-before的语义。JVM底层是通过一个叫做“内存屏障”的东西来完成。内存屏障,也叫做内存栅栏,是一组处理器指令,用于实现对内存操作的顺序限制。下面是完成上述规要求的内存屏障:
在NO的地方会插入指令屏障来防止指令重排序 。
观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令。lock前缀指令其实就相当于一个内存屏障。内存屏障是一组处理指令,用来实现对内存操作的顺序限制。volatile的底层就是通过内存屏障来实现的。下图是完成上述规则所需要的内存屏障。
各个指令的作用如下图。