再有人问你volatile是什么,把这篇文章也发给他。

上一篇文章中,咱们围绕volatile关键字作了不少阐述,主要介绍了volatile的用法、原理以及特性。在上一篇文章中,我提到过:volatile只能保证可见性和有序性,没法保证原子性。关于这部份内容,有读者阅读以后表示仍是不是很理解,因此我再单独写一篇文章深刻分析一下。java

volatile与有序性

在上一篇文章中咱们提到过:volatile一个强大的功能,那就是他能够禁止指令重排优化。经过禁止指令重排优化,就能够保证代码程序会严格按照代码的前后顺序执行。那么volatile又是如何禁止指令重排的呢?数据库

先给出结论:volatile是经过内存屏障来来禁止指令重排的。编程

**内存屏障(Memory Barrier)**是一类同步屏障指令,是CPU或编译器在对内存随机访问的操做中的一个同步点,使得此点以前的全部读写操做都执行后才能够开始执行此点以后的操做。下表描述了和volatile有关的指令重排禁止行为:缓存

从上表咱们能够看出:

当第二个操做是volatile写时,无论第一个操做是什么,都不能重排序。这个规则确保volatile写以前的操做不会被编译器重排序到volatile写以后。多线程

当第一个操做是volatile读时,无论第二个操做是什么,都不能重排序。这个规则确保volatile读以后的操做不会被编译器重排序到volatile读以前。架构

当第一个操做是volatile写,第二个操做是volatile读时,不能重排序。并发

具体实现方式是在编译期生成字节码时,会在指令序列中增长内存屏障来保证,下面是基于保守策略的JMM内存屏障插入策略:优化

  • 在每一个volatile写操做的前面插入一个StoreStore屏障。
    • 对于这样的语句Store1; StoreStore; Store2,在Store2及后续写入操做执行前,保证Store1的写入操做对其它处理器可见。
  • 在每一个volatile写操做的后面插入一个StoreLoad屏障。
    • 对于这样的语句Store1; StoreLoad; Load2,在Load2及后续全部读取操做执行前,保证Store1的写入对全部处理器可见。
  • 在每一个volatile读操做的后面插入一个LoadLoad屏障。
    • 对于这样的语句Load1; LoadLoad; Load2,在Load2及后续读取操做要读取的数据被访问前,保证Load1要读取的数据被读取完毕。
  • 在每一个volatile读操做的后面插入一个LoadStore屏障。
    • 对于这样的语句Load1; LoadStore; Store2,在Store2及后续写入操做被刷出前,保证Load1要读取的数据被读取完毕。

因此,volatile经过在volatile变量的操做先后插入内存屏障的方式,来禁止指令重排,进而保证多线程状况下对共享变量的有序性。

volatile与可见性

在上一篇文章中咱们提到过:Java中的volatile关键字提供了一个功能,那就是被其修饰的变量在被修改后能够当即同步到主内存,被其修饰的变量在每次是用以前都从主内存刷新。spa

其实,volatile对于可见性的实现,内存屏障也起着相当重要的做用。由于内存屏障至关于一个数据同步点,他要保证在这个同步点以后的读写操做必须在这个点以前的读写操做都执行完以后才能够执行。而且在遇到内存屏障的时候,缓存数据会和主存进行同步,或者把缓存数据写入主存、或者从主存把数据读取到缓存。操作系统

咱们在内存模型是怎么解决缓存一致性问题的?一文中介绍过缓存缓存一致性协议,同时也提到过内存一致性模型的实现能够经过缓存一致性协议来实现。同时,留了一个问题:已经有了缓存一致性协议,为何还须要volatile?

这个问题的答案能够从多个方面来回答:

一、并非全部的硬件架构都提供了相同的一致性保证,Java做为一门跨平台语言,JVM须要提供一个统一的语义。

二、操做系统中的缓存和JVM中线程的本地内存并非一回事,一般咱们能够认为:MESI能够解决缓存层面的可见性问题。使用volatile关键字,能够解决JVM层面的可见性问题。

三、缓存可见性问题的延伸:因为传统的MESI协议的执行成本比较大。因此CPU经过Store Buffer和Invalidate Queue组件来解决,可是因为这两个组件的引入,也致使缓存和主存之间的通讯并非实时的。也就是说,缓存一致性模型只能保证缓存变动能够保证其余缓存也跟着改变,可是不能保证马上、立刻执行。

  • 其实,在计算机内存模型中,也是使用内存屏障来解决缓存的可见性问题的(再次强调:缓存可见性和并发编程中的可见性能够互相类比,可是他们并非一回事儿)。写内存屏障(Store Memory Barrier)能够促使处理器将当前store buffer(存储缓存)的值写回主存。读内存屏障(Load Memory Barrier)能够促使处理器处理invalidate queue(失效队列)。进而避免因为Store Buffer和Invalidate Queue的非实时性带来的问题。

因此,内存屏障也是保证可见性的重要手段,操做系统经过内存屏障保证缓存间的可见性,JVM经过给volatile变量加入内存屏障保证线程之间的可见性。

内存屏障

再来总结一下Java中的内存屏障:用于控制特定条件下的重排序和内存可见性问题。Java编译器也会根据内存屏障的规则禁止重排序。

volatile与原子性

之前的文章中,咱们介绍synchronized的时候,提到过,为了保证原子性,须要经过字节码指令monitorentermonitorexit,可是volatile和这两个指令之间是没有任何关系的。volatile是不能保证原子性的。

网上有不少文章,拿i++的例子说明volatile不能保证原子性,而后进行各类分析,有的说因为引入内存屏障致使没法保证原子性,有的说一段i++代码,在编译后字节码为:

10: getfield      #2                  // Field i:I
    14: iconst_1
    15: iadd
    16: putfield      #2                  // Field i:I
复制代码

在不考虑内存屏障的状况下,一个i++指令也包含了四个步骤。

这些分析,只是说明了i++自己并非一个原子操做,即便使用volatile修饰i,也没法保证他是一个原子操做。并不能解释为何volatile为啥不能保证原子性。

要我说,因为CPU按照时间片来进行线程调度的,只要是包含多个步骤的操做的执行,自然就是没法保证原子性的。由于这种线程执行,又不像数据库同样能够回滚。若是一个线程要执行的步骤有5步,执行完3步就失去了CPU了,失去后就可能不再会被调度,这怎么可能保证原子性呢。

为何synchronized能够保证原子性 ,由于被synchronized修饰的代码片断,在进入以前加了锁,只要他没执行完,其余线程是没法得到锁执行这段代码片断的,就能够保证他内部的代码能够所有被执行。进而保证原子性。

可是synchronized对原子性保证也不绝对,若是真要较真的话,一旦代码运行异常,也没办法回滚。因此呢,在并发编程中,原子性的定义不该该和事务中的原子性同样。他应该定义为:一段代码,或者一个变量的操做,在没有执行完以前,不能被其余线程执行。

那么,为何volatile不能保证原子性呢?由于他不是锁,他没作任何能够保证原子性的处理。固然就不能保证原子性了。

总结

本文在上一篇文章的基础上,再次介绍了volatile和原子性、有序性以及可见性之间的关系。有序性和可见性是经过内存屏障实现的。而volatile是没法保证原子性的。

参考资料

深刻理解Java内存模型(四)——volatile

相关文章
相关标签/搜索