快速理解 volatile 关键字

前言

  看了不少 Java 并发编程书籍的目录,volatile 在 JMM 中老是单独拎出来做为一个章节来说,主要是由于它的特殊规则。要完全弄懂 volatile 不太容易,可是若是从它如何解决并发编程中的可见性、原子性和有序性问题来学习,就能很快掌握 volatile 的做用。学习 volatile 关键字颇有必要,Java 并发工具中的不少类都是基于 volatile 的。java

volatile 特性

  在 JMM 中 volatile 的三大特性以下:数据库

  1. 保证可见性:当写一个 volatile 变量时,JMM会把该线程对应的本地内存中的共享变量值刷新到主内存,使其余线程当即可见。
  2. 保证有序性:当变量被修饰为 volatile 时,JMM 会禁止读写该变量先后语句的大部分重排序优化,以保证变量赋值操做的顺序与程序中的执行顺序一致。
  3. 部分原子性:对任意单个 volatile 变量的读/写具备原子性,但相似于 volatile++ 这种复合操做不具备原子性。

如何保证可见性

  volatile 变量可见性不少书上都喜欢放到 happens-before 原则中来说:编程

对一个volatile域的写,happens-before于任意后续对这个volatile域的读。
复制代码

  其实我以为这句话初看并不能很好的理解 volatile 的可见性,并且还会引入新的概念 happens-before 规则。换一种表述方式会容易理解的多,其在 JMM 中的写和读语义以下:多线程

  1. 当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值刷新到主内存。
  2. 当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。

  这就保证了 volatile 变量的可见性,也解释了 happens-before 中的 volatile 规则,并且须要注意的是:在写和读时操做的是整个工做内存中的共享变量,因此在读 volatile 变量时工做内存中的其余共享变量也是最新的。并发

如何保证有序性

  volatile 的有序性可能比较晦涩,可是看完 JMM 针对编译器制定的 volatile 重排序规则表后就会很容易理解:app

  由上图 1 可知,JMM 限制了大部分状况下 volatile 变量读写语句先后语句的重排序,结合图片来看看下个这个例子:

class OrderingExample {
    int x = 0;
    volatile boolean flag = false;
    public void writer() {
        x = 42; //宇宙的终极答案
        flag = true;
    }
    public void reader() {
        if (flag == true) {
            //x = ?
        }
    }
}
复制代码

  以上代码在并发编程前传 中讲有序性的时候也贴过,这里将 flag 定义成 volatile。若是线程 A 先执行完 writer(),线程 B 后执行到 reader() 中的 x= 的时候,x 必定等于 42(JDK 1.5 之后),缘由以下:工具

  参考图 1,能够看出普通变量的写不能重排到 volatile 变量的写后面,因此便不存在有序性问题。 其余禁止重排序规则参考图 1 进行类推,整个规则让 JMM 在多线程环境下保证了 volatile 变量的有序性。在本规则中有如下两点须要注意:性能

  1. 只要 volatile 变量与普通变量之间的重排序可能会破坏 volatile 的内存语义,这种重排序就会被编译器重排序规则和处理器内存屏障插入策略禁止。换句话说,若是没有破坏 volatile 的内存语义则能够重排序,参考图 1 空白格子对应的规则。学习

  2. 为了实现volatile的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序,细则以下:优化

    在每一个volatile写操做的前面插入一StoreStore屏障。 
     在每一个volatile写操做的后面插入一个StoreLoad屏障。
     在每一个volatile读操做的后面插入一个LoadLoad屏障。
     在每一个volatile读操做的后面插入一个LoadStore屏障。
    复制代码

如何保证部分原子性

  一样拿并发编程前传中 dobule 和 long 的例子,double 和 long 变量的单个读/写在绝大部分商业虚拟机上都是原子的,但在在极端状况下并不具备原子性,而加了 volatile 后就必定能保证单个读/写原子性。这由 JMM 保证,其中底层原理有待深究,但底层应该是经过 cpu 指令来实现的。

  之因此说只能保证部分原子性,是由于 volatile 并不能保证 volatile 变量参与的复合语句的原子性,好比 i++; i+=1; 等这种看上去是单读和写,实质须要先读后写的语句。

与 synchronized 的区别

  因为 volatile 仅仅保证对单个 volatile 变量的读/写具备原子性,而锁的互斥执行的特性能够确保对整个临界区代码的执行具备原子性。在功能上,锁比 volatile 更强大;在可伸缩性和执行性能上,volatile 更有优点。若是读者想在程序中用volatile代替锁,请必定谨慎。即便是单个变量的语句,也只有如下三种状况下可使用 volatile 代替锁:

  1. 对变量的写入操做不依赖变量的当前值,或者你能确保只有单个线程更新变量的值。
  2. 该变量不会与其余状态变量一块儿归入不变性条件中。
  3. 在访问变量时不须要加锁。

  对于 1 的前半句是指对变量的写以前不能还要去读它,好比相似 i++、i = i + 1 等语句。至于 1 的后半句相似于咱们常见的一写多读模型,不存在多线程问题。

  对于 2 是指该变量不能与其余变量一块儿控制某个操做,好比 if( i < j ){},其中 i 和 j 都是共享变量,i 是 volatile 修饰的。又好比 while( i - j > 2){} 等。i 与其余共享变量 j 一块儿参与了不变的条件控制,故存在问题。

  在《Java 并发编程实战》中列出了第 3 点,而《深刻理解 Java 虚拟机》中直接删去了。可见对于 3 是不言而喻的。

总结

  了解 volatile 的三大特性之后,回看阿里数据库大牛何登成关于 volatile 的文章《C/C++ volatile关键词深度剖析》理解起来不要太简单。理解 volatile 简单,若是想灵活应用 volatile 能够看看 Java 并发工具包中的一些源码实现,看看大牛如何把 volatile 运用的恰到好处的。

相关文章
相关标签/搜索