深度解析Java中volatile的内存语义实现以及运用场景

 更新时间:2015年12月07日 17:59:16   作者:程晓明  
这篇文章主要介绍了Java中volatile的内存语义实现以及运用场景,通过JVM的机制来分析volatile关键字在线程编程中的作用,需要的朋友可以参考下

volatile内存语义的实现

下面,让我们来看看JMM如何实现volatile写/读的内存语义。

前文我们提到过重排序分为编译器重排序和处理器重排序。为了实现volatile内存语义,JMM会分别限制这两种类型的重排序类型。下面是JMM针对编译器制定的volatile重排序规则表:

2015127175655334.png (307×128)

举例来说,第三行最后一个单元格的意思是:在程序顺序中,当第一个操作为普通变量的读或写时,如果第二个操作为volatile写,则编译器不能重排序这两个操作。

从上表我们可以看出:

当第二个操作是volatile写时,不管第一个操作是什么,都不能重排序。这个规则确保volatile写之前的操作不会被编译器重排序到volatile写之后。
当第一个操作是volatile读时,不管第二个操作是什么,都不能重排序。这个规则确保volatile读之后的操作不会被编译器重排序到volatile读之前。
当第一个操作是volatile写,第二个操作是volatile读时,不能重排序。
为了实现volatile的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。对于编译器来说,发现一个最优布置来最小化插入屏障的总数几乎不可能,为此,JMM采取保守策略。下面是基于保守策略的JMM内存屏障插入策略:

  • 在每个volatile写操作的前面插入一个StoreStore屏障。
  • 在每个volatile写操作的后面插入一个StoreLoad屏障。
  • 在每个volatile读操作的后面插入一个LoadLoad屏障。
  • 在每个volatile读操作的后面插入一个LoadStore屏障。

上述内存屏障插入策略非常保守,但它可以保证在任意处理器平台,任意的程序中都能得到正确的volatile内存语义。

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

2015127175754358.png (485×313)

上图中的StoreStore屏障可以保证在volatile写之前,其前面的所有普通写操作已经对任意处理器可见了。这是因为StoreStore屏障将保障上面所有的普通写在volatile写之前刷新到主内存。

这里比较有意思的是volatile写后面的StoreLoad屏障。这个屏障的作用是避免volatile写与后面可能有的volatile读/写操作重排序。因为编译器常常无法准确判断在一个volatile写的后面,是否需要插入一个StoreLoad屏障(比如,一个volatile写之后方法立即return)。为了保证能正确实现volatile的内存语义,JMM在这里采取了保守策略:在每个volatile写的后面或在每个volatile读的前面插入一个StoreLoad屏障。从整体执行效率的角度考虑,JMM选择了在每个volatile写的后面插入一个StoreLoad屏障。因为volatile写-读内存语义的常见使用模式是:一个写线程写volatile变量,多个读线程读同一个volatile变量。当读线程的数量大大超过写线程时,选择在volatile写之后插入StoreLoad屏障将带来可观的执行效率的提升。从这里我们可以看到JMM在实现上的一个特点:首先确保正确性,然后再去追求执行效率。

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

2015127175810403.png (500×306)

上图中的LoadLoad屏障用来禁止处理器把上面的volatile读与下面的普通读重排序。LoadStore屏障用来禁止处理器把上面的volatile读与下面的普通写重排序。

上述volatile写和volatile读的内存屏障插入策略非常保守。在实际执行时,只要不改变volatile写-读的内存语义,编译器可以根据具体情况省略不必要的屏障。下面我们通过具体的示例代码来说明:

class VolatileBarrierExample {
  int a;
  volatile int v1 = 1;
  volatile int v2 = 2;

  void readAndWrite() {
    int i = v1;      //第一个volatile读
    int j = v2;      // 第二个volatile读
    a = i + j;      //普通写
    v1 = i + 1;     // 第一个volatile写
    v2 = j * 2;     //第二个 volatile写
  }

  …          //其他方法
}

针对readAndWrite()方法,编译器在生成字节码时可以做如下的优化:

2015127175838019.png (491×465)

注意,最后的StoreLoad屏障不能省略。因为第二个volatile写之后,方法立即return。此时编译器可能无法准确断定后面是否会有volatile读或写,为了安全起见,编译器常常会在这里插入一个StoreLoad屏障。

上面的优化是针对任意处理器平台,由于不同的处理器有不同“松紧度”的处理器内存模型,内存屏障的插入还可以根据具体的处理器内存模型继续优化。以x86处理器为例,上图中除最后的StoreLoad屏障外,其它的屏障都会被省略。

前面保守策略下的volatile读和写,在 x86处理器平台可以优化成:

2015127175854654.png (485×280)

前文提到过,x86处理器仅会对写-读操作做重排序。X86不会对读-读,读-写和写-写操作做重排序,因此在x86处理器中会省略掉这三种操作类型对应的内存屏障。在x86中,JMM仅需在volatile写后面插入一个StoreLoad屏障即可正确实现volatile写-读的内存语义。这意味着在x86处理器中,volatile写的开销比volatile读的开销会大很多(因为执行StoreLoad屏障开销会比较大)。

JSR-133为什么要增强volatile的内存语义

在JSR-133之前的旧Java内存模型中,虽然不允许volatile变量之间重排序,但旧的Java内存模型允许volatile变量与普通变量之间重排序。在旧的内存模型中,VolatileExample示例程序可能被重排序成下列时序来执行:

2015127175909083.png (453×368)

在旧的内存模型中,当1和2之间没有数据依赖关系时,1和2之间就可能被重排序(3和4类似)。其结果就是:读线程B执行4时,不一定能看到写线程A在执行1时对共享变量的修改。

因此在旧的内存模型中 ,volatile的写-读没有监视器的释放-获所具有的内存语义。为了提供一种比监视器锁更轻量级的线程之间通信的机制,JSR-133专家组决定增强volatile的内存语义:严格限制编译器和处理器对volatile变量与普通变量的重排序,确保volatile的写-读和监视器的释放-获取一样,具有相同的内存语义。从编译器重排序规则和处理器内存屏障插入策略来看,只要volatile变量与普通变量之间的重排序可能会破坏volatile的内存语意,这种重排序就会被编译器重排序规则和处理器内存屏障插入策略禁止。

由于volatile仅仅保证对单个volatile变量的读/写具有原子性,而监视器锁的互斥执行的特性可以确保对整个临界区代码的执行具有原子性。在功能上,监视器锁比volatile更强大;在可伸缩性和执行性能上,volatile更有优势。如果读者想在程序中用volatile代替监视器锁,请一定谨慎。

使用volatile关键字的场景

  synchronized关键字是防止多个线程同时执行一段代码,那么就会很影响程序执行效率,而volatile关键字在某些情况下性能要优于synchronized,但是要注意volatile关键字是无法替代synchronized关键字的,因为volatile关键字无法保证操作的原子性。通常来说,使用volatile必须具备以下2个条件:

  1)对变量的写操作不依赖于当前值

  2)该变量没有包含在具有其他变量的不变式中

  实际上,这些条件表明,可以被写入 volatile 变量的这些有效值独立于任何程序的状态,包括变量的当前状态。

  事实上,我的理解就是上面的2个条件需要保证操作是原子性操作,才能保证使用volatile关键字的程序在并发时能够正确执行。

  下面列举几个Java中使用volatile的几个场景。

1.状态标记量

volatile boolean flag = false;
 
while(!flag){
 doSomething();
}
 
public void setFlag() {
 flag = true;
}
 
volatile boolean inited = false;
//线程1:
context = loadContext(); 
inited = true;   
 
//线程2:
while(!inited ){
sleep()
}
doSomethingwithconfig(context);

 

2.double check

class Singleton{
 private volatile static Singleton instance = null;
  
 private Singleton() {
   
 }
  
 public static Singleton getInstance() {
  if(instance==null) {
   synchronized (Singleton.class) {
    if(instance==null)
     instance = new Singleton();
   }
  }
  return instance;
 }
}

相关文章

  • Java编程获取当前屏幕分辨率的方法示例

    Java编程获取当前屏幕分辨率的方法示例

    这篇文章主要介绍了Java编程获取当前屏幕分辨率的方法,涉及java针对系统硬件信息的相关操作技巧,需要的朋友可以参考下
    2017-08-08
  • Java泛型的使用限制实例分析

    Java泛型的使用限制实例分析

    这篇文章主要介绍了Java泛型的使用限制,结合实例形式分析了不能使用java泛型的情况以及泛型使用的相关注意事项,需要的朋友可以参考下
    2019-08-08
  • SpringBoot Security权限控制自定义failureHandler实例

    SpringBoot Security权限控制自定义failureHandler实例

    这篇文章主要为大家介绍了SpringBoot Security权限控制自定义failureHandler实例,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-11-11
  • 深入解析SpringBatch适配器

    深入解析SpringBatch适配器

    Spring Batch是Spring的一个子项目,使用Java语言并基于Spring框架为基础开发,使得已经使用 Spring 框架的开发者或者企业更容易访问和利用企业服务,本文给大家介绍SpringBatch适配器的相关知识,感兴趣的朋友一起看看吧
    2021-11-11
  • Java语言实现扫雷游戏(2)

    Java语言实现扫雷游戏(2)

    这篇文章主要为大家详细介绍了Java语言实现扫雷游戏第二部分代码,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2020-04-04
  • Java实现递归计算n的阶乘

    Java实现递归计算n的阶乘

    这篇文章主要为大家详细介绍了Java实现递归计算n的阶乘,利用递归的思想实现阶乘的计算,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2021-08-08
  • Java设计模式之共享模式/享元模式(Flyweight模式)介绍

    Java设计模式之共享模式/享元模式(Flyweight模式)介绍

    这篇文章主要介绍了Java设计模式之共享模式/享元模式(Flyweight模式)介绍,本文讲解了为什么使用共享模式/享元模式、如何使用共享模式/享元模式、Flyweight模式在XML等数据源中应用等内容,需要的朋友可以参考下
    2015-03-03
  • 基于Spring Security实现对密码进行加密和校验

    基于Spring Security实现对密码进行加密和校验

    我们在入门案例中,其实已经是一个非常简单的认证,但是用户名是写死的,密码也需要从控制台查看,很显然实际中并不能这么做,下面的学习中,我们来实现基于内存模型的认证以及用户的自定义认证,密码加密等内容,需要的朋友可以参考下
    2024-07-07
  • Java上传文件FTP服务器代码实例

    Java上传文件FTP服务器代码实例

    这篇文章主要介绍了Java上传文件FTP服务器代码实例,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2020-12-12
  • Java单例模式的创建,破坏和防破坏详解

    Java单例模式的创建,破坏和防破坏详解

    大家所熟知的单例模式只能创建唯一一个实例,今天我们介绍几种常见的单例模式,同时说一说如何破坏单例模式,同时又怎么来防破坏
    2021-09-09

最新评论