java - jmm之volatile特性

Posted cjunn

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了java - jmm之volatile特性相关的知识,希望对你有一定的参考价值。

volatile是什么?

volatile是JVM提供的一种轻量级的同步机制,其具有三个特性。

  • 保证可见性
  • 不保证原子性
  • 禁止指令重排

保证可见性

JMM(java memory model)中文翻译为Java内存模型,是JVM下的一种规范,规定了JVM对于程序在内存中的变量应该以什么样的一样形式进行访问。
在JMM中对于同步则有以下的规定:

  • 线程解锁前,必须把共享变量的值刷新回主内存。
  • 线程加锁前,必须读取主内存的最新值到自己的工作内存中。
  • 加锁与解锁需是同一把锁。
  • 线程间的通信(传值)必须通过主内存来完成。
    技术图片

  根据JMM中所规定的可知道每个执行线程都拥有各自线程的工作内存。对变量的计算操作实则是对各自工作内存内的变量副本进行操作,然后刷新到主内存中,而对于其他线程并不能感知到这个动作,仍然操作各自的工作内存内的变量副本,而此时就需要一种机制来通知每个线程说共享变量已经发生变动,不能在使用各自工作内存中旧的变量副本。这个机制就是volatile。

  当某个变量被volatile修饰后,当一个线程修改了共享变量的值,其他线程能够立即感知这个修改。volatile的特殊规则保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新该变量值。

  另外除了volatile关键字之外,Java还有两个关键字能实现可见性,即synchronized和final。而final关键字的可见性是指:被final修饰的字段在构造器中一旦初始化完成,并且构造器没有把“this”的引用传递出去(this引用逃逸是一件很危险的事情,其他线程有可能通过这个这个引用访问到“初始化了一半”对对象),那其他线程中就能看见final字段的值。这句话的意思是被final修饰的字段在构造方法是初始化完毕会立马刷新到主内存中,当构造器并没有把this传递出去但是有发生了this引用逃逸(JVM优化导致的指令重排序导致引用逃逸)其他线程就有可能拿到this的引用但是其构造函数却还没构造完毕,而被final字段的值具有可见性,那么在其他线程中就能够实时看见final字段的值。

不保证原子性

  这个相对比较好理解,每个线程在对于获取被volatile修饰的字段可以确保获取最新的值,但是由于在计算中并没有锁机制来确保1个时间点只进行一段计算而导致该线程准备更新该字段的值时候该其他线程对该字段设置的值直接覆盖导致数据错乱。所以对某个字段需要有一段的计算过程仅仅靠volatile是无法确保这段计算过程具有原子性,而是应该靠锁机制来确保。

顺序 线程A 线程B
1 b=a+1
2 b=a+1
3 a=b
4 a=b

  假设a为5那么即使a被volatile修饰着,他也只能确保线程在执行b=a+1时a的值是最新的值,但是当a未来得及设置进主内存中,线程A与线程B的b=a+1将等于6。接下来将6设置进主内存中,此时就发生数据错误。经过2个线程执行a的值应该是7而不是6。因此在计算过程中应该加上锁来避免这类情况发生。

禁止指令重排

  计算机在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排序,一般分为以下三种。
技术图片
  单线程环境里面确保程序最终执行结果和代码顺序执行的结果一致。
  处理器在进行重排序时必须要考虑指令之间的数据依赖性。
  多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致是无法确定的,结果无法预测。
因为指令重排将会带来的问题:

A线程指令重排导致B线程出错。

在线程A中:

context = loadContext();
inited = true;

在线程B中:

while(!inited ){ //根据线程A中对inited变量的修改决定是否使用context变量
   sleep(100);
}
doSomethingwithconfig(context);

假设线程A中发生了指令重排序:

inited = true;
context = loadContext();

那么B中很可能就会拿到一个尚未初始化或尚未初始化完成的context,从而引发程序错误。

指令重排导致单例模式失效。

public class Singleton {
  private static Singleton instance = null;
  private Singleton() {
        System.err.println("执行构造Singleton");
  }
  public static Singleton getInstance() {
     if(instance == null) {
        synchronzied(Singleton.class) {
           if(instance == null) {
               instance = new Singleton();  //非原子操作
           }
        }
     }
     return instance;
   }
}

  instance= new Singleton(),它并不是一个原子操作,它可以抽象为下面几条JVM指令:

  memory =allocate(); //1:分配对象的内存空间
  ctorInstance(memory); //2:初始化对象
  instance =memory; //3:设置instance指向刚分配的内存地址

  上面操作2依赖于操作1,但是操作3并不依赖于操作2,所以JVM是可以针对它们进行指令的优化重排序的,经过重排序后如下:

  memory =allocate(); //1:分配对象的内存空间
  instance =memory; //3:instance指向刚分配的内存地址,此时对象还未初始化
  ctorInstance(memory); //2:初始化对象

  而此时instance已分配到实例地址即instance不为null,但是却未初始化对象也就是未执行构造方法进行初始化。此时另外一个线程进入该方法获取到未执行构造方法的对象进行使用就有可能导致程序报错。因此需要instance前修饰volatile来避免引用逃逸这类情况发生。

以上是关于java - jmm之volatile特性的主要内容,如果未能解决你的问题,请参考以下文章

Java并发多线程编程——volatile关键字

JUC - 多线程之JMM;volatile

Java_17:volatile和AtomicInteger

自己动手写把”锁”之---JMM和volatile

Java进阶—— volatile JMM双重检查以及创建单例对象的时候为什么要加volatile关键字?

Java进阶——volatile + JMM + 双重检查创建单例对象的时候为什么要加volatile关键字?!!