volatile原理

目录

volatile 是如何来保证可见性的呢? 

volatile 的两条实现原则

volatile 的内存语义

volatile 写-读建立的 happens-before 关系

 volatile 写-读的内存语义

 volatile 内存语义的实现

基于保守策略的 JMM 内存屏障插入策略


        volatile保证共享变量的可见性和有序性。比 synchronized 的使用和执行成本更低,因为它不会引起线程上下 文的切换和调度。

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

CPU 术语的定义 

volatile 是如何来保证可见性的呢? 

对 volatile 进行写操作时,CPU 会做什么事情

instance = new Singleton(); // instance 是 volatile 变量

转变成汇编代码,如下。

0x01a3de1d: movb $0×0,0×1104800(%esi);0x01a3de24: lock addl $0×0,(%esp);

Lock 前缀的指令在多核处理器下会引发了两件事情 。

⚫ 将当前处理器缓存行的数据写回到系统内存。

⚫ 这个写回内存的操作会使在其他 CPU 里缓存了该内存地址的数据无效。

        在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了。

当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态。

当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存里

volatile 的两条实现原则

Lock 前缀指令会引起处理器缓存回写到内存。

Lock 前缀指令导致在执行指令期间,声言处理器的 LOCK#信号。

在多处理器环境中,LOCK#信号确保在声言该信号期间,处理器可以独占任何共享内存。(锁总线)

但是,在最近的处理器里,LOCK#信号一般不锁总线,而是锁缓存,毕竟锁总线开销的比较大。 在 8.1.4 节有详细说明锁定操作对处理器缓存的影响,对于 Intel486 和 Pentium 处理器, 在锁操作时,总是在总线上声言 LOCK#信号。

但在 P6 和目前的处理器中,如果访问的内存区域已经缓存在处理器内部,则不会声言 LOCK#信号。相反,它会锁定这块内存区域的缓存并回写到内存,并使用缓存一致性机制来确保修改的原子性,此操作被称为“缓存锁定”,缓存一致性机制会阻止同时修改由两个以上处理器缓存的内存区域数据。

一个处理器的缓存回写到内存会导致其他处理器的缓存无效。

IA-32 处理器和 Intel 64 处理器使用 MESI(修改、独占、共享、无效)控制协议去维护内部缓存和其他处理器缓存的一致性。

在多核处理器系统中进行操作的时候,IA-32 和 Intel 64 处理器能嗅探其他处理器访问系统内存和它们的内部缓存。

处理器使用嗅探技术保证它的内部缓存、系统内存和其他处理器的缓存的数据在总线上保持一致。例如, 在 Pentium 和 P6 family 处理器中,如果通过嗅探一个处理器来检测其他处理器打算写内存地址,而这个地址当前处于共享状态,那么正在嗅探的处理器将使它的缓存行无效, 在下次访问相同内存地址时,强制执行缓存行填充。

volatile 的内存语义

volatile 变量自身具有下列特性。

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

⚫ 原子性:对任意单个 volatile 变量的读/写具有原子性(即使是 64 位的 long 型 和 double 型变量,只要它是 volatile 变量,对该变量的读/写就具有原子性。,但类似于 volatile++这种复合操作不具有原子性。

volatile 写-读建立的 happens-before 关系

volatile 变量的写-读可以实现线程之间的通信。

从内存语义的角度来说,volatile 的写-读与锁的释放-获取有相同的内存效果: volatile 写和锁的释放有相同的内存语义;volatile 读与锁的获取有相同的内存语义。

 volatile 写-读的内存语义

volatile 写的内存语义如下:当写一个 volatile 变量时,JMM 会把该线程对应的本地内存中的共享变量值刷新到主内存。

volatile 读的内存语义如下:当读一个 volatile 变量时,JMM 会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。

 volatile 内存语义的实现

为了实现 volatile 内存语义,JMM 会分别限制这两种类型的重 排序类型。

volatile 重排序规则表

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

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

⚫ 当第一个操作是 volatile 写,第二个操作是 volatile 读时,不能重排序。


基于保守策略的 JMM 内存屏障插入策略

⚫在每个 volatile 写操作的前面插入一个 StoreStore 屏障。

⚫ 在每个 volatile 写操作的后面插入一个 StoreLoad 屏障。

⚫ 在每个 volatile 读操作的后面插入一个 LoadLoad 屏障。

⚫ 在每个 volatile 读操作的后面插入一个 LoadStore 屏障。

volatile 写插入内存屏障后生成的指令序列示意图

StoreStore 屏障可以保证在 volatile 写之前,其前面的所有普通写操作已经对任意处理器可见了。这是因为 StoreStore 屏障将保障上面所有的普通写在 volatile 写之前刷新到主内存。

StoreLoad 屏障的作用是避免 volatile 写与后面可能有的 volatile 读/写操作重排序。因为编译器常常无法准确判断在一 个 volatile 写的后面是否需要插入一个 StoreLoad 屏障(比如,一个 volatile 写之后方法立 即 return)。

为了保证能正确实现 volatile 的内存语义,JMM 在采取了保守策略:

在每个 volatile 写的后面,或者在每个 volatile 读的前面插入一个 StoreLoad 屏障。从整体执行效率的角度考虑,JMM 最终选择了在每个 volatile 写的后面插入一个 StoreLoad 屏障。

因为 volatile 写-读内存语义的常见使用模式是:一个写线程写 volatile 变量,多个读线程读同 一个 volatile 变量。当读线程的数量大大超过写线程时,选择在 volatile 写之后插入 StoreLoad 屏障将带来可观的执行效率的提升。


volatile 读插入内存屏障后生成的指令序列示意图

LoadLoad 屏障用来禁止处理器把上面的 volatile 读与下面的普通读重排 序。

LoadStore 屏障用来禁止处理器把上面的 volatile 读与下面的普通写重排序。


在实际执行时,只要不 改变 volatile 写-读的内存语义,编译器可以根据具体情况省略不必要的屏障。

class VolatileBarrierExample {
     int a;
     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 写
     }
     … // 其他方法
}
编译器在生成字节码时可以做的优化

最后的 StoreLoad 屏障不能省略。因为第二个 volatile 写之后,方法立即 return。此时编译器可能无法准确断定后面是否会有 volatile 读或写,为了安全起见,编 译器通常会在这里插入一个 StoreLoad 屏障。

        由于 volatile 仅仅保证对单个 volatile 变量的读/写具有原子性,而锁的互斥执行的特性可以确保对整个临界区代码的执行具有原子性。在功能上,锁比 volatile 更强大;在可伸缩性和执行性能上,volatile 更有优势。如果想在程序中用 volatile 代替锁,请一 定谨慎,具体详情请参阅 Brian Goetz 的文章《Java 理论与实践:正确使用 Volatile 变量》。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值