深入探索JVM高效并发 — Java内存模型(二)

对于volatile型变量的特殊规则

关键字volatile可以说是Java虚拟机提供的最轻量级的同步机制

volatile到底意味着什么?Java内存模型为volatile专门定义了一些特殊的访问规则

关键字volatile的作用

当一个变量被定义成volatile之后,它将具备两项特性:

第一项是保证此变量对所有线程的可见性,这里的可见性是指当一条线程修改了这个变量的值,新值对于其他线程来说是可以立即得知的。

而普通变量并不能做到这一点,普通变量的值在线程间传递时均需要通过主内存来完成。比如,线程A修改一个普通变量的值,然后向主内存进行回写,另外一条线程B在线程A回写完成了之后再对主内存进行读取操作,新变量值才会对线程B可见。

基于volatile变量的运算在并发下并不是线程安全的

由于volatile变量只能保证可见性(还有有序性,此处还未讲到),在不符合以下两条规则的运算场景中,我们仍然要通过加锁(使用synchronizedjava.util.concurrent中的锁或原子类)来保证原子性

·运算结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值。

·变量不需要与其他的状态变量共同参与不变约束。

第二个语义是禁止指令重排序优化(指令重排序会干扰程序的并发执行)。

普通的变量仅会保证在该方法的执行过程 中所有依赖赋值结果的地方都能获取到正确的结果,而不能保证变量赋值操作的顺序与程序代码中的执行顺序一致。因为在同一个线程的方法执行过程中无法感知到这点,这就是Java内存模型中描述的所谓线程内表现为串行的语义Within-Thread As-If-Serial Semantics)。

volatile修饰的变量,赋值后(前面mov%eax0x150(%esi)这句便是赋值操作)多执行了一个“lock addl$0x0(%esp)”操作,这个操作的作用相当于一个内存屏障Memory BarrierMemory Fence指重排序时不能把后面的指令重排序到内存屏障之前的位置。(注意不要与第3章中介绍的垃圾收集器用于捕获变量访问的内存屏障互相混淆

只有一个处理器访问内存时,并不需要内存屏障;但如果有两个或更多处理器访问同一块内存,且其中有一个在观测另一个,就需要内存屏障来保证一致性了(应该是有序性吧?)

(volatile可见性的实现原理)

这句指令中的“addl$0x0(%esp)”(把ESP寄存器的值加0)显然是一个空操作,这里的关键在于lock前缀,它的作用是将本处理器的缓存写入了内存,该写入动作也会引起别的处理器或者别的内核无效化(Invalidate)其缓存,这种操作相当于对缓存中的变量做了一次前面介绍Java内存模式中所说的“storewrite”操作。所以通过这样一个空操作,可让前面volatile变量的修改对其他处理器立即可见。

(volatile禁止指令重排序优化即有序性的实现原理)

从硬件架构上讲,指令重排序是指处理器采用了允许将多条指令不按程序规定的顺序分开发送给各个相应的电路单元进行处理。但并不是说指令任意重排,处理器必须能正确处理指令依赖情况保障程序能得出正确的执行结果。

所以在同一个处理器中,重排序过的代码看起来依然是有序的。因此,lock addl$0x0(%esp)指令把修改同步到内存时,意味着所有之前的操作都已经执行完成(那么在此之前的代码都是有序的),这样便形成了指令重排序无法越过内存屏障的效果。

选用volatile的意义:

某些情况下,volatile的同步机制的性能确实要优于锁(使用synchronized关键字或java.util.concurrent包里面的锁),但是由于虚拟机对锁实行的许多消除和优化,使得我们很难确切地说volatile就会比synchronized快上多少。

volatile自己与自己比较,那可以确定一个原则:volatile变量读操作的性能消耗与普通变量几乎没有什么差别,但是写操作则可能会慢上一些,因为它需要在本地代码中插入许多内存屏障指令来保证处理器不发生乱序执行。不过即便如此,大多数场景下volatile的总开销仍然要比锁来得更低。我们在volatile与锁中选择的唯一判断依据仅仅是volatile的语义能否满足使用场景的需求。

Java内存模型中对volatile变量定义的特殊规则的定义:

保证可见性:

·线程T对变量Vuse动作可以认为是和线程T对变量Vloadread动作相关联的,必须连续且一起出现并且还要按照read、load、use的顺序。 (即与普通的变量不同)

这条规则要求在工作内存中,每次使用V前都必须先从主内存刷新最新的值,用于保证能看见其他线程对变量V所做的修改。

·只有当线程T对变量V执行的前一个动作是assign的时候,线程T才能对变量V执行store动作;并

且,只有当线程T对变量V执行的后一个动作是store的时候,线程T才能对变量V执行assign动作。线程T对变量Vassign动作可以认为是和线程T对变量Vstorewrite动作相关联的,必须连续且一起出现。

这条规则要求在工作内存中,每次修改V后都必须立刻同步回主内存中,用于保证其他线程可以看到自己对变量V所做的修改。

保证有序性:

·假定动作A是线程T对变量V实施的useassign动作,假定动作F是和动作A相关联的loadstore动作,假定动作P是和动作F相应的对变量Vreadwrite动作;与此类似,假定动作B是线程T对变量W实施的useassign动作,假定动作G是和动作B相关联的loadstore动作,假定动作Q是和动作G相应的对变量Wreadwrite动作。如果A先于B,那么P先于Q

这条规则要求volatile修饰的变量不会被指令重排序优化,从而保证代码的执行顺序与程序的顺序相同

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值