volatile 如何实现保证可见性和有序性

volatile 与可见性:

先说一下可见性,所谓的可见性就是指可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。

缓存一致性协议
现代处理器为了提高处理速度,在处理器和内存之间增加了多级缓存,处理器不会直接去和内存通信,将数据读到内部缓存中再进行操作。由于引入了多级缓存,就存在缓存数据不一致问题。
什么是缓存一致性协议呢?
每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器要对这个数据进行修改操作的时候,会强制重新从系统内存里把数据读到处理器缓存里。
volatile是两条实现原则:
1.Lock前缀指令会引起处理器缓存会写到内存
当对volatile变量进行写操作的时候,JVM会向处理器发送一条lock前缀的指令,将这个缓存中的变量回写到系统主存中
2.一个处理器的缓存回写到内存会导致其他处理器的缓存失效
处理器使用嗅探技术保证内部缓存 系统内存和其他处理器的缓存的数据在总线上保持一致。

综合上面两条实现原则,我们了解到:如果一个变量被volatile所修饰的话,在每次数据变化之后,其值都会被强制刷入主存。而其他处理器的缓存由于遵守了缓存一致性协议,也会把这个变量的值从主存加载到自己的缓存中。这就保证了一个volatile在并发编程中,其值在多个缓存中是可见的。
为了保证内存的可见性,除了缓存一致性协议还有一个happends-before关系

注意:
数组与对象实例中的 volatile,针对的是引用,对象获数组的地址具有可见性,但是数组或对象内部的成员改变不具备可见性。

volatile写—读建立的happends-before关系
volatile的写—读与锁的释放—获取有着相同的内存效果,所以说一个volatile变量的单个读/写操作,与一个普通变量的读/写操作都是使用同一个锁来同步,执行效果是一样的。先简单介绍一下happends-before
happends-before法则
1.程序次序法则:按照代码顺序执行
2.监视器锁法则:一个unlock操作要先于同一个锁的lock操作
3.volatile变量法则:对volatile域的写入操作happends-before于每一个后续对同一域的读操作
4.线程启动法则:在一个线程里,对Thread.start()的调用会先于Thread.run();
5.线程终结法则:线程中的任何动作都happends-before于其他线程检测到这个线程已经终结,或者从Thread.join 调用中成功返回,或者Thread.isAlive返回false
6.中断法则:一个线程调用另一个线程的interrupt.happens-before于被中断的线程发现中断。(通过跑出interruptedException,或者调用isInterrupted和interrupted)
7.终结法则:一个对象的构造函数的结束happends-before于这个对象finalizer的开始。
8.传递性:如果A happens-before于B, 且B happends-before 于C, 则A happens-before 于C

volatile与有序性

我们都知道多线程通过抢占时间片来执行自己的代码体,所以我们会感觉到线程是同时执行完的,除了引入了时间片以外,由于处理器优化和指令重排等,CPU还可能对输入代码进行乱序执行,比如我们拿到数据要执行写库,查询,删除这三个操作,这就会可能要涉及到有序性的问题了。

volatile可以禁止指令重排,这就保证了代码的程序会严格按照代码的先后顺序执行。这就保证了有序性。被volatile修饰的变量的操作,会严格按照代码顺序执行接下来我们就说一下为了实现volatile内存语义JMM是怎样限制重排序(包括编译器重排序和处理器重排序)的。

volatile重排序规则表(针对编译器重排序):

在这里插入图片描述

从这张表我们可以看出:
当第一个操作是Volatile读时,不管第二个操作是什么,都不能重排序;
当第一个操作是Volatile写时,第二个操作是Volatile读或写,不能重排序;
当第一个操作是普通读写,第二个操作是Volatile写时,不能重排序。

内存屏障(针对处理器重排序):
编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。(首先保证了正确性,再去追求执行效率)
1.在每个volatile写操作前插入StoreStore屏障;对于这样的语句Store1; StoreLoad; Store2,在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见。
2.在每个volatile写操作后插入StoreLoad屏障;对于这样的语句Store1; StoreLoad; Load2,在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。
3.在每个volatile读操作前插入LoadLoad屏障;对于这样的语句Load1;LoadLoad; Load2,在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。
4.在每个volatile读操作后插入LoadStore屏障;对于这样的语句Load1; LoadStore; Store2,在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。
如果编译器无法确定后面是否还会有volatile读或者写的时候,为了安全,编译器通常会在这里插入一个StoreLoad屏障
在这里插入图片描述

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值