JMM模型与volatile详解(二)

volatile

volatile是Java虚拟机提供的轻量级的同步机制。

  • 不能保证原子性
  • 保证volatile修饰的共享变量对所有线程是可见的。也就是当一个线程修改 了一个被volatile修饰共享变量的值,新值总是可以被其他线程立即得知。
  • 禁止指令重排序

禁止指令重排的原因

volatile关键字另一个作用就是禁止指令重排优化,从而避免多线程环境下程序出现乱 序执行的现象,关于指令重排优化前面已详细分析过,这里主要简单说明一下volatile是如 何实现禁止指令重排优化的。先了解一个概念,内存屏障(Memory Barrier)。

硬件层的内存屏障

Intel硬件提供了一系列的内存屏障,主要有:

  1. lfence,是一种Load Barrier 读屏障
  2. sfence, 是一种Store Barrier 写屏障
  3. mfence, 是一种全能型的屏障,具备ifence和sfence的能力
  4. Lock前缀,Lock不是一种内存屏障,但是它能完成类似内存屏障的功能。Lock会对 CPU总线和高速缓存加锁,可以理解为CPU指令级的一种锁。它后面可以跟ADD, ADC, AND, BTC, BTR, BTS, CMPXCHG, CMPXCH8B, DEC, INC, NEG, NOT, OR, SBB, SUB, XOR, XADD, and XCHG等指令

不同硬件实现内存屏障的方式不同,Java内存模型屏蔽了这种底层硬件平台的差异,由 JVM来为不同的平台生成相应的机器码。 JVM中提供了四类内存屏障指令:
在这里插入图片描述

  • 内存屏障,又称内存栅栏,是一个CPU指令,它的作用有两个,一是保证特定操作的执 行顺序,二是保证某些变量的内存可见性(利用该特性实现volatile的内存可见性)。由于 编译器和处理器都能执行指令重排优化。如果在指令间插入一条Memory Barrier则会告诉 编译器和CPU,不管什么指令都不能和这条Memory Barrier指令重排序,也就是说通过插 入内存屏障禁止在内存屏障前后的指令执行重排序优化。
  • Memory Barrier的另外一个作用 是强制刷出各种CPU的缓存数据,因此任何CPU上的线程都能读取到这些数据的最新版本。 总之,volatile变量正是通过内存屏障实现其在内存中的语义,即可见性和禁止重排优化。

volatile内存语义的实现

前面提到过重排序分为编译器重排序和处理器重排序。为了实现volatile内存语义, JMM会分别限制这两种类型的重排序类型。
下图是JMM针对编译器制定的volatile重排序规则表。
在这里插入图片描述
举例来说,第二行最后一个单元格的意思是:在程序中,当第一个操作为普通变量的读或写 时,如果第二个操作为volatile写,则编译器不能重排序这两个操作。

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

实现volatile的内存语义

为了实现volatile的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障 来禁止特定类型的处理器重排序。对于编译器来说,发现一个最优布置来最小化插入屏障的 总数几乎不可能。为此,JMM采取保守策略。下面是基于保守策略的JMM内存屏障插入策略

  1. ∙在每个volatile写操作的前面插入一个StoreStore屏障。
  2. ∙在每个volatile写操作的后面插入一个StoreLoad屏障。 ∙
  3. 在每个volatile读操作的后面插入一个LoadLoad屏障。 ∙
  4. 在每个volatile读操作的后面插入一个LoadStore屏障

上述内存屏障插入策略非常保守,但它可以保证在任意处理器平台,任意的程序中都能得到 正确的volatile内存语义。

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

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

这里比较有意思的是,volatile写后面的StoreLoad屏障。此屏障的作用是避免volatile 写与 后面可能有的volatile读/写操作重排序。因为编译器常常无法准确判断在一个volatile 写的后面 是否需要插入一个StoreLoad屏障(比如,一个volatile写之后方法立即 return)。为了保证能正确 实现volatile的内存语义,JMM在采取了保守策略:在每个volatile写的后面,或者在每个volatile 读的前面插入一个StoreLoad屏障。从整体执行效 率的角度考虑,JMM最终选择了在每个 volatile写的后面插入一个StoreLoad屏障。因为 volatile写-读内存语义的常见使用模式是:一个 写线程写volatile变量,多个读线程读同一 个volatile变量。当读线程的数量大大超过写线程时,选择在volatile写之后插入StoreLoad 屏障将带来可观的执行效率的提升。从这里可以看到JMM 在实现上的一个特点:首先确保 正确性,然后再去追求执行效率。

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

在这里插入图片描述
上图中LoadLoad屏障用来禁止处理器把上面的volatile读与下面的普通读重排序。 LoadStore屏障用来禁止处理器把上面的volatile读与下面的普通写重排序。
上述volatile写和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写 11 } 12 }

针对readAndWrite()方法,编译器在生成字节码时可以做如下的优化。
在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值