Java内存模型-volatile的内存语义

一 引言

  据说在Java 5以前volatile关键字备受争议,因此本文也不讨论1.5版本以前的volatile。本文主要针对1.5后即JSR-133针对volatile作了强化后的了解。html

二 volatile的特性

  开门见山,volatile变量自身具备如下特性:app

  • 可见性(最重要的特性)。对一个volatile变量的读,老是能看到(任意线程)对这个volatile变量最后的写入。
  • 原子性。对任意(包括64位long类型和double类型)单个volatile变量的读/写具备原子性。可是类型于a++这种复合操做不具备原子性。

  下面经过案例来证实下可见性,先看一个普通变量是否能保证可见性:ide

 3 class Example {
 4     private boolean stop = false; 5 public void execute() { 6 int i = 0; 7 System.out.println("thread1 start loop."); 8 while(!getStop()) { 9 i++; 10  } 11 System.out.println("thread1 finish loop,i=" + i); 12  } 13 public boolean getStop() { 14 return stop; // 对普通变量的读 15  } 16 public void setStop(boolean flag) { 17 this.stop = flag; // 对普通变量的写 18  } 19 } 20 public class VolatileExample { 21 public static void main(String[] args) throws Exception { 22 final Example example = new Example(); 23 Thread t1 = new Thread(new Runnable() { 24  @Override 25 public void run() { 26  example.execute(); 27  } 28  }); 29  t1.start(); 30 31 Thread.sleep(1000); 32 System.out.println("主线程即将置stop值为true..."); 33 example.setStop(true); 34 System.out.println("主线程已将stop值为:" + example.getStop()); 35 System.out.println("主线程等待线程1执行完..."); 36 37  t1.join(); 38 System.out.println("线程1已执行完毕,整个流程结束..."); 39  } 40 }

  上面程序的意思是:让线程1先执行而后主(main)线程修改标志看是否能让子线程跳出循环。执行程序后发现程序并无执行完,而是在等待线程1执行完毕。这就说明主线程修改stop变量并不对线程1可见,因此普通变量是不保证可见性的
oop

  当你把变量stop用volatile修饰时,主线程修改stop变量会立马对线程1可见并终止程序,这就证实volatile变量是具备可见性特性的。下面修改后的结果。测试

  原子性特性已经说的很清楚了(对任意(包括64位long类型和double类型)单个volatile变量的读/写具备原子性),记着是对单个volatile变量的读或写才具备原子性(若是要进行测试的话,将上面案例的volatile变量修改为long/double类型,测试逻辑同样,只不过将它放在x86的机器上运行。由于在x86的机器上不能保证long类型和double类型的原子性的,具体缘由在Java内存模型中的顺序一致性一节有说明)。另外任何复合操做都不能保证原子性,如a++,a = a+1, a = b。特别注意a = b这类,它实际上包含2个操做,它先要去读取b的值,再将b的值写入工做内存,虽然读取b的值以及将b的值写入工做内存这2个操做都是原子性操做,可是合起来就不是原子性操做了this

  想要理解透volatile特性有一个很好的方法,就是把对volatile变量的单个读/写,当作是使用同一个锁对这些单个读/写操做作了同步。编码

三 volatile写-读创建的happens-before关系

  这个详细在happens-before规则中说明。spa

四 volatile写-读的内存语义

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

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

  以上面VolatileExample程序为例进行简单说明,当主线程对stop进行修改后且子线程还没有对stop进行读时,主线程已经把stop的值刷新到了主内存。其示意图以下:

  当子线程进行读取时,会把本地内存置为无效直接去主内存中读取。(这里的主线程和子线程能够了解为两个普通线程没有父子关系)其示意图以下:

五 volatile内存语义的实现

  为了实现volatile的内存语义,JMM会分别限制这两种类型的重排序。下图是JMM针对编译器指定的volatile重排序规则表。

  • 当第二个操做为volatile写操做时,无论第一个操做是什么(普通读写或者volatile读写),都不能进行重排序。这个规则确保volatile写以前的全部操做都不会被重排序到volatile写以后;
  • 当第一个操做为volatile读操做时,无论第二个操做是什么,都不能进行重排序。这个规则确保volatile读以后的全部操做都不会被重排序到volatile读以前;
  • 当第一个操做是volatile写操做时,第二个操做是volatile读操做,不能进行重排序。

  为了实现 volatile 的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障(在JMM也有提到过且有说明对应的几种屏障的做用,请仔细阅读)来禁止特定类型的处理器重排序。下面是基于保守策略的 JMM 内存屏障插入策略:

  • 在每一个 volatile 写操做的前面插入一个 StoreStore 屏障(禁止前面的写与volatile写重排序)。

  • 在每一个 volatile 写操做的后面插入一个 StoreLoad 屏障(禁止volatile写与后面可能有的读和写重排序)

  • 在每一个 volatile 读操做的后面插入一个 LoadLoad 屏障(禁止volatile读与后面的读操做重排序)

  • 在每一个 volatile 读操做的后面插入一个 LoadStore 屏障(禁止volatile读与后面的写操做重排序)

   其中重点说下StoreLaod屏障,它是确保可见性的关键,由于它会将屏障以前的写缓冲区中的数据所有刷新到主内存中。上述内存屏障插入策略很是保守,但它能够保证在任意处理平台,任意的程序中都能获得正确的volatile语义。下面是保守策略(为何说保守呢,由于有些在实际的场景是可省略的)下,volatile 写操做 插入内存屏障后生成的指令序列示意图:

  其中StoreStore屏障能够保证在volatile写以前,其前面的全部普通写操做对任意处理器可见(把它刷新到主内存)。另外volatile写后面有StoreLoad屏障,此屏障的做用是避免volatile写与后面可能有的读或写操做进行重排序。由于编译器经常没法准确判断在一个volatile写的后面是否须要插入一个StoreLoad屏障(好比,一个volatile写以后方法当即return)为了保证能正确实现volatile的内存语义,JMM采起了保守策略:在每一个volatile写的后面插入一个StoreLoad屏障。由于volatile写-读内存语义的常见模式是:一个写线程写volatile变量,多个度线程读同一个volatile变量。当读线程的数量大大超过写线程时,选择在volatile写以后插入StoreLoad屏障将带来可观的执行效率的提高。从这里也可看出JMM在实现上的一个特色:首先确保正确性,而后再去追求效率(其实咱们工做中编码也是同样)。

  下面是在保守策略下,volatile读插入内存屏障后生产的指令序列示意图:

  上述volatile写和volatile读的内存屏障插入策略很是保守。在实际执行时,只要不改变volatile写-读的内存语义,编译器能够根据具体状况忽略没必要要的屏障。在JMM基础中就有提到过各个处理器对各个屏障的支持度,其中x86处理器仅会对写-读操做作重排序。

六 总结

  volatile主要做用是具备可见性和原子性(单个变量),其实现原理就是利用屏障来保障实现。要想完全掌握就应该多作下相关场景的编码,经典的场景有:状态标记量、volatile方式的double check等。

  以上若有错误之处,欢迎指出,欢迎讨论,谢谢!

相关文章
相关标签/搜索