浅谈Volatile关键字

浅谈Volatile关键字

  1. volatile的定义

在并发编程中,volatile和synchronized都是非常重要的组成,可以这样定义volatile,它是轻量级的synchronized,像是synchronized保证了原子性,可见性,有序性,但是volatile只保证了可见性和有序性,并不能保证原子性
保证可见性的意思是指,在多线程的情况下,一个线程修改被volatile修饰的共享变量时,另一个线程可以读到修改的这个值

  1. volatile解析

这里可以带入两行代码
instence = new Singleton();这里的instence是被volatile修饰过的
这里转成汇编语言如下在这里插入图片描述

可以清晰地看到多出了一行指令,Lock前缀指令做了两件事
将当前的数据缓存行写回到系统内存
这个写回的操作,会使在其他的cpu中缓存了该地址的数据无效
这里的原因是因为,为了提高处理的速度,处理器不和内存直接进行通信,而是将系统内存中的数据读取到缓存进行操作,但是这里就碰到了一个问题,在我写完数据之后,我不知道什么时候会写回到内存,这里就会出现问题,所以Lock的前缀指令主要就执行了一个事情,就是将缓存修改的数据写回到主存中。但是这里又出现了一个问题,其他的处理器拿到的数据还是过时的数据,并不是最新的,在进行处理的时候就会出现数据问题,所以这里实现了缓存一致性协议(MESI,下面会对缓存一致性协议有详细介绍),解决的方式就变成了,处理器在总线上进行嗅探,看我拿到的数据是不是最新的,不是的话我就将当前的设置成无效,重新从主存上读取数据

通过总线锁来保证原子性
所谓处理器总线锁就是使用处理器提供的一个LOCK#信号,当一个处理器总线上输出此信号时,其它处理器的请求将被阻塞住,那么该处理器可以独占共享内存。
通过缓存锁定来保证原子性
总线锁定把CPU和内存之间的通信锁住了,这使得锁定期间,其它处理器不能操作其它内存地址的数据,所以总线锁的开销比较大,所以就引进了缓存锁定来代替总线锁定来进行优化。
所谓“缓存锁定”是指内存区域如果被缓存在处理器的缓存中,那么当它执行锁操作会写内存时,处理器不需要再总线上加锁,而是修改内存地址,并允许处理器的缓存一致性协议“来保证操作的原子性,因为缓存一致性协议会阻止同时修改由两个以上处理器缓存区的内存区域数据,当其它处理器回写已被修改的缓存行数据实,会使其它缓存行无效。

MESI缓存一致性协议
MESI中每个缓存行都有四个状态,分别是E(exclusive)、M(modified)、S(shared)、I(invalid)。下面我们介绍一下这四个状态分别代表什么意思。
M:代表该缓存行中的内容被修改了,并且该缓存行只被缓存在该CPU中。这个状态的缓存行中的数据和内存中的不一样,在未来的某个时刻它会被写入到内存中(当其他CPU要读取该缓存行的内容时。或者其他CPU要修改该缓存对应的内存中的内容时(个人理解CPU要修改该内存时先要读取到缓存中再进行修改),这样的话和读取缓存中的内容其实是一个道理)。
E:E代表该缓存行对应内存中的内容只被该CPU缓存,其他CPU没有缓存该缓存对应内存行中的内容。这个状态的缓存行中的内容和内存中的内容一致。该缓存可以在任何其他CPU读取该缓存对应内存中的内容时变成S状态。或者本地处理器写该缓存就会变成M状态
S:该状态意味着数据不止存在本地CPU缓存中,还存在别的CPU的缓存中。这个状态的数据和内存中的数据是一致的。当有一个CPU修改该缓存行对应的内存的内容时会使该缓存行变成 I 状态。
I:代表该缓存行中的内容时无效的。

  1. 重排序

重排序可以分为三种
第一种,编译器优化的重排序,会在不改变单线程语义的情况下,来进行语句重排序
第二种,指令集并行的重排序,在并发的情况下,如果不存在数据之间的依赖性,处理器是可以对其进行语句的重排序的
第三种,内存系统的重排序,处理器使用缓存和读写缓冲区,这就使得加载和存储像是乱序
在这里插入图片描述
重排序的顺序
上面的1是JMM编译器进行的重排序,23是处理器进行的重排序,这些重排序会导致在多线程的情况下出现内存的可见性问题
对于编译器的重排序,JMM编译器会禁止一些特定类型的编译器重排序
而对于处理器的重排序,他会对JMM的编译器进行生成指令序列的时候,插入特定类型的内存屏障,来禁止一些特定类型处理器的重排序
通过上面的操作来保证内存的一致性和可见性

  1. 数据依赖性

两个操作访问一个变量,一个是读操作,一个是写操作,这时候两个操作就存在数据依赖性
在这里插入图片描述
这里前面有说过,编译器和处理器可能会对操作进行重排序,但是在重排序的时候会遵守数据依赖性,所以编译器和处理器不会对存在数据依赖性的两个操作进行修改
这里的数据依赖性只针对于单线程单处理器

  1. as-if-serial

这里的语义就是:在单线程的情况下,无论怎么重排序,都不会对执行结果造成改变
但是如果两个操作之间不存在数据依赖性,那么他就有可能被编译器和处理器进行重排序
as-if-serial这里给了程序员一种虚假的感觉,就是我的程序是按照顺序来执行的
不可以规避掉对多线程的影响,多线程情况下重排序依旧会影响到结果

  1. volatile内存语义

volatile的特性
可见性,对于任意一个volatile变量的读,总可以看见这个变量最后的写
原子性,对于任意一个volatile变量的读/写都具有原子性,但是类似于volatile++这种符合操作并不适用

  1. 内存屏障

Load:将内存存储的数据拷贝到处理器的缓存中。
Store:将处理器缓存的数据刷新到内存中。
LoadLoad:前一条指令是读取,后一条指令也是读取。
LoadStore:前一条指令是读取,后一条指令是写入。
StoreLoad:前一条指令是写入,后一条指令是读取。
StoreStore:前一条指令是写入,后一条指令也是写入。
这里是引用
这里可以引申出来几条
当第二个操作是volatile写时,不管第一个操作是什么,都不能进行重排序,这个规则确保volatile写之前的操作不会被编译器优化到volatile之后
当第一个操作是volatile读时,不管第二个操作是什么,都不可以进行重排序,这个规则确保了volatile之后的操作不会被重排序到volatile读之前
第一个是volatile读,第二个是volatile写时不能进行重排序

  1. happens-before

JMM内存模型
主要是为了为程序员提供足够强的内存可见性保证
对编译器和处理器的限制要尽可能放松
JMM将happens-before要求禁止的重排序分成了两种
会改变的程序结果的重排序
不会改变结果的重排序
JMM根据这两种不同顺序的重排序,制定了两种不同的处理策略
对于会改变的结果的重排序,编译器和处理器要禁止掉
对于不会改变结果的重排序,JMM对编译器和处理器不做要求
在这里插入图片描述
happens-before定义
①如果一个操作happens-before另一个操作,那么第一个操作将对第二个操作可见,而且第一个操作的执行顺序在第二个之前
②两个操作之间存在happens-before关系,不代表具体时间必须按照这个顺序来执行,如果重排序之后的结果和happens-before关系执行相同,那么这种重排序并不非法
上面的①是java(JMM)内存模型的保证
上面的②是JMM对编译器和处理器重排序的约束原则
happens-before规则
1)程序顺序规则:一个线程中的每个操作,happens-before与该线程中的任意后续操作。
2)监视器锁规则:对一个锁的解锁,happens-before与随后对这个锁的加锁。
3)volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。
4)传递性:如果A happens-before B,且B happens-before C,那么A happens-before C。
5)start()规则:如果线程A 执行操作ThreadB.start()(线程B启动),那么A线程的ThreadB.start()操作happens-before于线程B中的任意操作。
6)join规则:如果线程A执行操作ThreadB.join()并成功返回,那么线程B中的任意操作happens-before于线程A从ThreadB.join()操作成功的返回。

  1. DCL问题

这里就是一个很常见的问题,单例模式的情况下创建开销大对象,并且降低锁同步的开销
在这里插入图片描述
这里看起来是没什么问题,但是在实际使用的情况下,会出现创建了不止一个实例的情况,这是怎么产生的呢?
这就是因为在new instance()这个操作并不是原子性的,在另一个线程进来判断的时候,可能创建的对象还没有创建完
可以分解为如下
在这里插入图片描述
其中这三行代码可能会出现重排序的情况
在这里插入图片描述
这种情况就是JMM对编译器和处理器的支持,他并没有改变最终结果,所以并不会禁止重排序
解决的方法
对于volatile修饰的变量来说,禁止重排序,保障运行过程中写操作在读操作之前,如此就可以保障(1)–>(2)–>(3)的顺序,所以也不会产生上述情况了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值