一、volatile的特性
1.volatile可见性;对一个volatile的读,总可以看到对这个变量最终的写;
2.volatile原子性;volatile对单个读/写具有原子性(32位Long、Double),但是复合操作除外,例如i++;
3.JVM底层采用“内存屏障”来实现volatile语义;
二、volatile与happens-before
示例:
class VolatileTest1 {
int i = 0;
volatile boolean flag = false;
// Thread A
public void write(){
i = 2; //1
flag = true; //2
}
// Thread B
public void read(){
if(flag){ //3
System.out.println("---i = " + i); //4
}
}
}
依据happens-before原则,就上面程序得到如下关系:
1.依据happens-before程序顺序原则:1 happens-before 2、3 happens-before 4;
2.根据happens-before的volatile原则:2 happens-before 3;
3.根据happens-before的传递性:1 happens-before 4 操作1、操作4存在happens-before关系,那么1一定是对4可见的。
volatile除了保证可见性外, 还有就是禁止重排序。所以A线程在写volatile变量之前所有可见的共享变量,在线程B读同一个 volatile变量后,将立即变得对线程B可见。
三、volataile的内存语义及其实现
在JMM中,线程之间的通信采用共享内存来实现的。volatile的内存语义是:
1.当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值立即刷新到主内存中。
2.当读一个volatile变量时,JMM会把该线程对应的本地内存设置为无效,直接从主内存中读取共享变量.
所以volatile的写内存语义是直接刷新到主内存中,读的内存语义是直接从主内存中读取。
volatile的内存语义是如何实现?
对于一般的变量则会被重排序,而对于volatile则不能,这样会影响其内存语义,所以为了实现volatile的内存语义JMM会限制重排序。
其重排序规则如下:
1.如果第一个操作为volatile读,则不管第二个操作是啥,都不能重排序。这个操作确保volatile读之后的操作不会被编译器重排序到volatile读之前;
2.当第二个操作为volatile写是,则不管第一个操作是啥,都不能重排序。这个操作确保volatile写之前的操作不会被编译器重排序到volatile写之后;
3.当第一个操作volatile写,第二操作为volatile读时,不能重排序。
volatile的底层实现是通过插入内存屏障,但是对于编译器来说,发现一个最优布置来最小化插入内存屏障的总数几乎是不可能的,
所以,JMM采用了保守策略。如下:
1.在每一个volatile写操作前面插入一个StoreStore屏障
2.在每一个volatile写操作后面插入一个StoreLoad屏障
3.在每一个volatile读操作后面插入一个LoadLoad屏障
4.在每一个volatile读操作后面插入一个LoadStore屏障
StoreStore屏障可以保证在volatile写之前,其前面的所有普通写操作都已经刷新到主内存中。
StoreLoad屏障的作用是避免volatile写与后面可能有的volatile读/写操作重排序。
LoadLoad屏障用来禁止处理器把上面的volatile读与下面的普通读重排序。
LoadStore屏障用来禁止处理器把上面的volatile读与下面的普通写重排序。
示例
class VolatileTest2 {
int a = 0;
volatile int v1 = 1;
volatile int v2 = 2;
void readAndWrite(){
int i = v1; //volatile读
int j = v2; //volatile读
a = i + j; //普通读
v1 = i + 1; //volatile写
v2 = j * 2; //volatile写
}
}
以上