并发编程—Volatile关键字

锁提供了两种主要特性:互斥(mutual exclusion) 和可见性(visibility)。互斥即一次只容许一个线程持有某个特定的锁,所以能够保证一次就只有一个线程在访问共享数据。可见性要复杂一些,它必须确保释放锁以前对共享数据作出的更改对于随后得到该锁的另外一个线程是可见的。java

volatile 变量能够被看做是一种 “轻量级的 synchronized”,与 synchronized 块相比,volatile 变量所需的编码较少,而且运行时开销也较少,可是它所能实现的功能也仅是 synchronized 的一部分。编程

volatile变量

一个共享变量被volatile修饰以后,则具备了两层语义:缓存

  1. 保证了该变量在多个线程的可见性。
  2. 禁止了指令重排序

保证内存可见性

前面讲过Java内存模型,能够知道:对一个共享变量进行操做时,各个线程会将共享变量从主内存中拷贝到工做内存,而后CPU会基于工做内存中的数据进行处理。线程在工做内存进行操做完成以后什么时候会将结果写回主内存中?这个时机对普通变量是没有规定的。因此才致使了内存可见性问题。安全

volatile是如何解决可见性问题的?
若是代码中的共享变量被volatile修饰,在生成汇编代码时会在volatile修饰的共享变量进行写操做的时候会多出Lock前缀的指令。在多核处理器的状况下,这个Lock指令主要有3个功能:并发

  1. volatile的变量被修改后会当即写入到主存中
  2. 这个写回主存的操做会告知其它线程中该变量对应的缓存行失效,因此其它线程若是要操做这个变量,会从新去主存中读取最新的值。
  3. 禁止特定类型的重排序。

因此,被volatile修饰的变量可以保证每一个线程可以获取该变量的最新值,从而避免出现数据脏读的现象app

禁止指令重排序

对于volatile的共享变量,编译器在生成字节码时,会在指令序列中插入内存屏障(Lock指令)来禁止特定类型的重排序。这是在happens-before的原则下作进一步的约束性能

对于编译器来讲,发现一个最优布置来最小化插入屏障的总数几乎是不可能的,为此,JMM采起了保守策略:编码

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

须要注意的是:volatile写是在前面和后面分别插入内存屏障,而volatile读操做是在后面插入两个内存屏障。atom

  • StoreStore屏障:禁止上面的普通写和下面的volatile写重排序;
  • StoreLoad屏障:防止上面的volatile写与下面可能有的volatile读/写重排序
  • LoadLoad屏障:禁止下面全部的普通读操做和上面的volatile读重排序
  • LoadStore屏障:禁止下面全部的普通写操做和上面的volatile读重排序

以下两张图来自《Java并发编程的艺术》一书:spa

  • volatile变量的写操做
    volatile写
  • volatile变量的读操做
    volatile读

根据上面的说明也能得出:虽然volatile关键字能禁止指令重排序,可是volatile也只能在必定程度上保证有序性。在volatile以前和以后的指令集不会乱序越过volatile变量执行,但volatile以前和以后的指令集在没有关联性的前提下,仍然会执行指令重排。

使用 volatile 变量的条件

volatile并不能代替synchronized,要使 volatile 变量提供理想的线程安全,必须同时知足下面两个条件:

  1. 对变量的写操做不依赖于当前值
    例如i++的操做就没法经过volatile保证结果准确性的,由于i++包含了读取-修改-写入三个步骤,并非一个原子操做,因此 volatile 变量不能用做线程的安全计数器。
    例以下面的这段代码,能够说明volatile变量的操做不具备原子性

    package com.lzumetal.multithread.volatiletest;
    
    public class Counter {
    
      private volatile static int count = 0;
    
      private static void inc() {
          //延迟1毫秒,使得结果明显
          sleep(1);
          count++;
      }
    
      public static void main(String[] args) {
    
          //同时启动1000个线程,去进行i++计算,看看实际结果
          for (int i = 0; i < 1000; i++) {
              new Thread(Counter::inc).start();
          }
    
          sleep(1000);   
          System.out.println("运行结果:Counter.count=" + Counter.count); //结果极可能<1000
      }
    
      private static void sleep(long millis) {
          try {
              Thread.sleep(millis);
          } catch (InterruptedException e) {
              e.printStackTrace();
          }
      }
    
    }

    运行计数器的结果很大可能性是<1000的。对于计数器的这种功能,通常是须要使用JUC中atomic包下的类,利用CAS的机制去作。

  2. 该变量没有包含在具备其余变量的不变式中
    这句话有点拗口,看代码比较直观。

    public class NumberRange {
          private volatile int lower = 0;
           private volatile int upper = 10;
    
          public int getLower() { return lower; }
          public int getUpper() { return upper; }
    
          public void setLower(int value) { 
              if (value > upper) 
                  throw new IllegalArgumentException(...);
              lower = value;
          }
    
          public void setUpper(int value) { 
              if (value < lower) 
                  throw new IllegalArgumentException(...);
              upper = value;
          }
      }

上述代码中,上下界初始化分别为0和10,假设线程A和B在某一时刻同时执行了setLower(8)setUpper(5),且都经过了不变式的检查,设置了一个无效范围(8, 5),因此在这种场景下,须要使setLower()setUpper()操做原子化 —— 而将字段定义为 volatile 类型是没法实现这一目的的。

使用 volatile 举例

虽然使用 volatile 变量要比使用相应的锁简单得多,并且性能也更好,可是通常不会太多的使用它,主要是它比使用锁更加容易出错。
想要安全地使用volatile,必须牢记一条原则:只有在状态真正独立于程序内其余内容时才能使用 volatile

修饰状态标志量

volatile boolean shutdownRequested;
 
...
 
public void shutdown() { 
    shutdownRequested = true; 
}
 
public void doWork() { 
    while (!shutdownRequested) { 
        // do stuff
    }
}

在这个示例使用 synchronized 块编写循环要比使用 volatile 状态标志编写麻烦不少。因为 volatile 简化了编码,而且状态标志并不依赖于程序内任何其余状态,所以此处很是适合使用 volatile。

double-check 单例模式

public class Singleton {

    private volatile static Singleton instance;

    public static Singleton getInstance() {
        if (instance == null) {                 //1
            syschronized(Singleton.class) {     //2
                if (instance == null) {         //3
                    instance = new Singleton(); //4
                }
            }
        }
        return instance;
    } 
}

为何要用volatile修饰才是最安全的呢?可能有人会以为是这样:线程1执行完第4步,释放锁。线程2得到锁后执行到第4步,因为可见性的缘由,发现instance仍是null,从而初始化了两次。
可是不会存在这种状况,由于synchronized能保证线程1在释放锁以前会讲对变量的修改刷新到主存当中,线程2拿到的值是最新的。

实际存在的问题是无序性。
第4步这个new操做是无序的,它可能会被编译成:
a.先分配内存,让instance指向这块内存
b.在内存中建立对象

synchronized虽然是互斥的,但不表明一次就把整个过程执行完,它在中间是可能释放时间片的,时间片不是锁。也就是说可能在a执行完后,时间片被释放,线程2执行到1,此时它读到的instance是否是null呢?基于可见性,多是null,也可能不是null。 有意思的是,在这个例子中,若是读到的是null,反而没问题了,接下来会等待锁,而后再次判断时不为null,最后返回单例。 若是读到的不是null,按代码逻辑直接return instance,但这个instance还没执行构造参数,因此使用的时候就会出现问题。

相关文章
相关标签/搜索