JAVA内存模式-4-volatile

volatile特性

可见性:对一个volatile变量的读,总是能看到任意线程对这个volatile变量最后的写入。

原子性:对任意单个volatile变量的读写具有原子性,但类似于volatile++这种复合操作不具有原子性。

从happens-before角度来看volatile的读写

可见性来看,volatile的写-读,与锁的释放-获取具有相同的内存效果:volatile写和锁释放、volatile读与锁获取具有相同的内存效果。

class volatileTest{

int a = 0;

volatile boolean flag = false;

public void init(){

a = 1;//操作1

flag = true;//操作2

}

public void print(){

if(flag){

int j = a;//操作3

System.out.println(j++);//操作4

}

}

}

从happens-before来看:

  1. 程序顺序规则:1 happens-before 2,3 happens-before 4。
  2. volatile变量规则:2 happens-before 3。
  3. 传递性:1 happens-before 4。

黑色箭头表示程序顺序规则;

橙色箭头表示volatile规则;

蓝色箭头表示组合这些规则后提供的happens-before保证。

volatile读写的内存过程

volatile写的时候,JMM会把该线程对应的本地内存中的共享变量值刷新到主内存。

volatile读的时候,JMM会把该线程对应的本地内存置为无效后,线程接下来再从主内存中读取共享变量。

如何实现volatile小村效果?

因为JAVA、CPU会对代码、字节码执行重排序,因此这里就从编译器重排序、处理器重排序来分析。为了实现volatile内存效果,JMM会分别限制这2种类型的重排序类型。

JMM针对编译器制定的volatile重排序规则:

是否能重排序

第二个操作

第一个操作

普通读写

Volatile读

Volatile写

普通读写

 

 

NO

Volatile读

NO

NO

NO

Volatile写

 

NO

NO

从上表看出:

  1. 当第二个操作是volatile写时,不管第一个操作是什么,都不能重排序从而确保volatile写之前的操作不会被编译器排序到volatile写之后。
  2. 当第一个操作是volatile读时,不管第二个操作是什么,这个规则确保volatile读之后的操作不会被编译器重排序到volatile读之前。
  3. 当第一个操作是volatile写,第二个操作是volatile读时,不能重排序。

JMM针对CPU重排序制定的volatile重排序规则:

编译器在生成字节码文件时会在指令序列中插入内存屏障。但对于编译器而言如何插入最少的内存屏障指令呢?对不起,无法确定,只能是本着“宁可错杀一千不可放过一个”来插入内存屏障指令:(优化是服从于JSR-133规范的,首先确保JSR-133规范,然后再谈优化)

  1. 分别在每个volatile写操作的前面插入一个StoreStore屏障、后面插入一个StoreLoad屏障。
  2. 分别在每个volatile读操作的后面依次插入一个LoadLoad屏障、一个LoadStore屏障。

volatile写

volatile写前面的StoreStore屏障指令能前面所有普通读写的结果刷新到主内存,这样所有的CPU中缓存的数据均一致。

volatile写后面的StoreLoad屏障指令则非常有趣,为什么是StoreLoad呢?因为考虑到volatile写后面可能与volatile读/写操作做重排序,或者,volatile写后面直接就是return的话(return之后就没有操作了必须对其他CPU可见,如果不是return的话,你也得必须对其他CPU可见),为了能及时刷新到主内存来确保所有CPU可见,JMM这里采用保守策略:在每个volatile写的后面、每个volatile读的前面插入一个StoreLoad屏障来达成volatile写的内存效果。

另外还可以从一个常见场景来做解释:JMM选择在每个volatile写的后面插入一个StoreLoad屏障指令,是因为常常是一个写线程写volatile变量,N个读线程读同一个volatile变量,当N 大大超过写线程时(此时写线程就1个而已),通过在volatile写后面的StoreLoad来确保写的结果及时刷新到主内存进而提高执行效率。

volatile读

LoadLoad屏障指令确保禁止CPU把volatile读与下面的普通读做重排序。

LoadStore屏障指令确保禁止CPU把volatile读与下面的普通写做重排序。

(我觉得,这里可以总结出一句话:为了实现volatile读写的内存可见性,写必须确保其准备工作的可见性以及结果的可见性,读必须确保其优先性)

volatile发展历史

int a = 0;

volatile boolean flag = false;

public void writer(){

a = 1;

flag = true;

}

public void reader(){

if(flag){

int i = a;

......

}

}

JSR-133旧内存模型中,不允许volatile变量之间重排序但允许volatile变量与普通变量重排序,对于上述代码中可能存在如下情况:

Writer方法中a和flag不存在数据依赖性,因此,重排序可能出现的情况是:flag=true;a=1;

那么假如A线程执行writer方法,B线程执行reader方法,那么显而易见,结果不是我们想要的结果。

为了避免这个问题发生,JSR-133在新内存模型中对volatile进行了强化:

严格限制JMM和CPU对volatile变量与普通变量的重排序,确保volatile的写-读和锁的释放-获取具有相同的内存语义。

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值