并发编程--volatile详解

 一、volatile的定义

Java 言允 许线 访问 共享 量, 了确保共享变 量能被准确和一致地更新, 线 应该 确保通 排他 锁单 量。 Java 言 提供了volatile ,在某些情况下比 要更加方便。如果一个字段被声明成 volatile Java 线 程内存模型确保所有线 程看到 量的 是一致的。

二、Java内存模型(JMM)

现代计算机的内存模型

其实早期计算机中cpu和内存的速度是差不多的,但在现代计算机中,cpu的指令速度远超内存的存取速度,由于计算机的存储设备与处理器的运算速度有几个数量级的差距,所以现代计算机系统都不得不加入一层读写速度尽可能接近处理器运算速度的高速缓存(Cache)来作为内存与处理器之间的缓冲。将运算需要使用到的数据复制到缓存中,让运算能快速进行,当运算结束后再从缓存同步回内存之中,这样处理器就无须等待缓慢的内存读写了。

基于高速缓存的存储交互很好地解决了处理器与内存的速度矛盾,但是也为计算机系统带来更高的复杂度,因为它引入了一个新的问题:缓存一致性(CacheCoherence)。在多处理器系统中,每个处理器都有自己的高速缓存,而它们又共享同一主内存(MainMemory)。

Java内存模型规定:所有变量都需要存储在主内存中,线程工作内存保存了变量在主内存中的副本,线程对变量的所有操作都在工作内存中进行,执行结束后在同步到主内存中去。这里必然会存在时间差,在这个时间差内,该线程对副本的操作,对于其他线程是不见的,从而造成了可见性问题。

 三、指令重排序

        在执 行程序 了提高性能, 编译 器和 理器常常会 指令做重排序。重排序分 3
型。
1 编译 化的重排序。 编译 器在不改 变单线 程程序 语义 的前提下,可以重新安排
序。
2 )指令 并行的重排序。 理器采用了指令 并行技 Instruction-Level
Parallelism ILP )来将多条指令重叠 行。如果不存在数据依 性, 理器可以改 变语 对应
机器指令的 序。
3 )内存系 的重排序。由于 理器使用 存和 / 冲区, 使得加 和存 操作看上
去可能是在乱序 行。
Java 源代 到最 终实际执 行的指令序列,会分 别经历 下面 3 种重排序
         这些重排序可能会 致多 线 程程序出现 内存可 问题 编译 器, JMM 编译 器重排序 规则 会禁止特定 型的 编译 器重排序(不是所有的编译 器重排序都要禁止)。 理器重排序, JMM 理器重排序 规则 会要求Java 编译 器在生成指令序列 ,插入特定 型的内存屏障 指令,通 内存屏障指令来禁止特定 型的 理器重排序。

 as-if-serial

不管怎么重排序,单线程下的执行结果不能被改变。

编译器、runtime和处理器都必须遵守as-if-serial语义。

四、volatile的作用

上述讲了两个多线程存在的问题,那么volatile是如何解决这两个问题的呢

1.保证共享变量的可见性:

使用volatile修饰的变量,任何线程对其进行操作都是在主内存中进行的,不会产生副本,从而保证共享变量的可见性。

2.防止局部指令重排序:

happens-before规则中的volatile变量规则规定了一个线程先去写一个volatile变量,然后一个线程去读这个变量,那么这个写操作的结果一定对读的这个线程可见。

五、MESI(缓存一致性协议)

volatile修饰的变量是存在于主内存中所以是多CPU共享的,那么当CPU写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号通知其他CPU将该变量的缓存行置为无效状态,因此当其他CPU需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取。

嗅探

每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存里。

嗅探缺点--总线风暴

由于Volatile的MESI缓存一致性协议,需要不断的从主内存嗅探和cas不断循环,无效交互会导致总线带宽达到峰值。

所以不要大量使用Volatile,至于什么时候去使用Volatile什么时候使用锁,根据场景区分。

六、Volatile保证不会被执行重排序原理

1.内存屏障

java编译器会在生成指令系列时在适当的位置会插入内存屏障指令来禁止特定类型的处理器重排序。

为了实现volatile的内存语义,JMM会限制特定类型的编译器和处理器重排序,JMM会针对编译器制定volatile重排序规则表:

2.写屏障

volatile写是在前面和后面分别插入内存屏障

3.读屏障

volatile读操作是在后面插入两个内存屏障。 

 

4. happens-before

针对重排序原则,为了提高处理速度,JVM会对代码进行编译优化,也就是指令重排序优化,并发编程下指令重排序会带来一些安全隐患:如指令重排序导致的多个线程操作之间的不可见性。如果让程序员再去了解这些底层的实现以及具体规则,那么程序员的负担就太重了,严重影响了并发编程的效率。从JDK5开始,提出了happens-before的概念,通过这个概念来阐述操作之间的内存可见性。

如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须存在happens-before关系。

volatile域规则:对一个volatile域的写操作,happens-before于任意线程后续对这个volatile域的读。

如果现在我的变了flag变成了false,那么后面的那个操作,一定要知道我变了。

意味着 对 一个volatile 量的 是能看到(任意 线 程) 对这 volatile 量最后的写入。

七、volatile缺点

volatile是无法保证原子性的,就是一次操作,要么完全成功,要么完全失败。

假设现在有N个线程对同一个变量进行累加也是没办法保证结果是对的,因为读写这个过程并不是原子性的。

要解决也简单,要么用原子类,比如AtomicInteger,要么加锁(记得关注Atomic的底层)。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值