Java内存模型JMM之四volatile关键字

前言

volatile关键字是JVM提供的最轻量级的同步机制,但是由于其不容易被正确地、完整的理解,以至于许多程序员都习惯不去使用它,而总是选择synchronized重量级的锁机制来进行同步,本文将弄清楚volatile关键字的真正语义。

当一个变量被定义成volatile之后,它将具备两种语义特性:可见性有序性

一、可见性

当一条线程修改了一个volatile变量的值,新值是立即对其他线程可知的。这是普通变量所不能保证的,通过前文的JMM内存模型可知,普通变量的值在线程之间由于各个线程有各自的工作内存的原因,并不能做到随时同步。

       导致volatile不能被容易理解的地方也就在这里,虽然对一个volatile变量的读取,都能保证获取到任意线程对这个volatile变量最后的写入,但是对volatile变量的复合操作(例如volatile++,  volatile= volatile * x)仍然不具有原子性因为volatile++这种复合操作实际上包含三个操作:读取、加1、将加1的结果赋值回写。volatile关键字只能保证第一个操作“读取”的结果是正确的,但是在执行后面两个操作的时候,其他线程依然可以甚至已经改变了volatile变量的值,使的现在操作的volatile变量已经是过期的数据。Volatile只能保证对修饰的变量的单次读或者写操作是原子性的(包括long和double类型)。

       因此在不符合以下两条规则的运算场景中,我们仍然要通过加锁来保证对volatile变量操作的原子性。

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

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

如下这种场景就非常适合使用volatile变量来控制并发。当shutdown()方法被调用时,能保证所有线程中执行的doWork()方法都能立即停下来。

volatile boolean shutdownRequested;

public void shutdown(){
      shutdownRequested = true;
}

public void doWork(){
      while(!shutdownRequested){
             //do stuff
      }
}

总的来说,当一个变量被volatile修饰后,表示着线程本地内存无效,当一个线程修改共享变量后他会立即被更新到主内存中,当其他线程读取共享变量时,它会直接从主内存中读取。 

二、有序性

volatile变量的有序性通过禁止指令重排序优化来保证(volatile屏蔽指令重排序的语义在JDK1.5中才被完全修复,此前的JDK中及时将变量声明为volatile,也仍然不能完全避免重排序所导致的问题)。通过前文的重排序内容我们知道,普通的变量仅仅会保证在方法执行过程中所有依赖该变量赋值结果的地方通过禁止重排序保证都能获取正确的结果,而如果变量的赋值操作不被后面的操作所依赖,由于在方法的执行过程中无法感知到变量值的改变,所以这时候是可以进行重排序的,也就是as-if-serial语义所描述的行为。通过如下代码示例可以说明为何指令重排序可能会干扰程序的并发执行:

Map configOptions;

// 此变量必须定义为volatile 
volatile boolean initialized = false;
// 假设以下代码在线程A中执行
// 模拟读取配置信息,读取完成之后,设置initialized为true来通知其他线程配置可用

configOptions  = readConfigOptions(fileName);
initialized = true;

// 假设以下代码在线程B中执行
// 等待initialized 为true,代表线程A已经初始化完配置信息
while(!initialized){
    sleep();
}

// 使用线程A初始化好的配置信息
doSomethingUseConfig();

如果initialized变量没有被定义为volatile,就可能由于指令重排序的优化导致最后一句的“initialized = true”被提前执行,从而线程B中使用配置信息的代码就可能出现错误,而volatile关键字可以避免这样的情况

三、底层实现原理

通过JMM之二内存屏障章节我们也可以知道,内存屏障可以禁止指令重排序以及影响数据可见性,这不正是volatile关键字的两层语义吗?其实,JVM底层就是采用“内存屏障”来实现volatile的语义。 JMM针对编译器制定了如下的volatile重排序限制策略:  
是否能重排序 第二个操作
 第一个操作  普通读/写  volatile读  volatile写
 普通读/写      NO
 volatile读  NO  NO  NO
 volatile写    NO  NO
  1. 当第一个操作是volatile读时,不论第二个操作是什么,都不能重排序。
  2. 当第二个操作是volatile写时,不论第一个操作是什么,都不能重排序。
  3. 当第一个操作是volatile写,第二个操作是volatile读或写时,亦不能重排序。
        为了实现以上策略,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序,下面是基于保守策略的JMM内存屏障插入策略(在实际中,只要不改变volatile写-读得内存语义,编译器可以根据具体情况优化,省略不必要的屏障):  

猜你喜欢

转载自pzh9527.iteye.com/blog/2400166