场景

volatile这个在多线程使用时能保证线程间的可见性。具体怎么用呢?举个例子:


public class VolatileVisibilityTest {    private static  boolean initFlag = false;    //private static volatile boolean initFlag = false;
   public static void main(String[] args) throws InterruptedException {        new Thread(() -> {            System.out.println("waiting data");            while (!initFlag) {
           }            System.out.println("========success!!!");        }).start();

       Thread.sleep(2000);
       new Thread(() -> prepareData()).start();

   }
   private static void prepareData() {        System.out.println("开始修改initFlag...");        initFlag = true;        System.out.println("initFlag修改成功...");    }}


如上面的代码,结果是什么呢?会不会打印 “========success!!!”。我们来看结果


"C:\Program Files\Java\jdk1.8.0_212\bin\java.exe" ...waiting data开始修改initFlag...initFlag修改成功...


程序一直在whlie循环中,如果想要走出while循环,需要对initFlag 添加Volatile关键字修饰

"C:\Program Files\Java\jdk1.8.0_212\bin\java.exe" ...waiting data开始修改initFlag...initFlag修改成功...========success!!!


如上结果,加了Volatile后会打印 “========success!!!”。


JMM线程模型

Java关键字——volatile底原理分析_java


如上图,线程获取数据是先从主内存中获取,然后将数据拷贝到工作内存。如果A线程改变了共享变量,此时只是在工作内存中改变了数据。对于其他线程来说是不可见的。


那共享变量加了关键字后为什么会可见了呢?

Java关键字——volatile底原理分析_java_02


在解读上面图之前我们先来解释下几个指令操作。

read(读取):从主内存读取数据load(载入):将主内存读取到的数据写入工作内存use(使用):从工作内存读取数据来计算assign(赋值):将计算好的值重新赋值到工作内存中store(存储):将工作内存数据写入主内存write(写入):将store过去的变量值赋值给主内存中的变量lock(锁定):将主内存变量加锁,标识为线程独占状态unlock(解锁):将主内存变量解锁,解锁后其他线程可以锁定该变量

   

volatile保证可见性

如上图:线程1和线程2去主内存中拿(通过read指令)数据initFlag并将其放在(通过load指令)各自的工作内存中,此时initFlag的值为false。当线程2要修改initFlag的值为true,需要经历下面几步:


1)将工作内存中的initFlag 值加载(use指令)到线程2的执行引擎中

2)执行引擎将initFlag的值改为true

3)   将修改的值assign到工作内存中

4)将工作内存中的值store到主内存中

5)将主内存的变量赋值(通过write命令)

在第4步store时,数据会经过cpu总线,这时线程1会嗅探到值的变化。就会从主内存中获取新值。等等,这里可能你会有疑问,如果新值经过总线还没到达主内存中,这时线程1就去主内存中获取值,还是以前的旧值啊。那系统是怎么解决的呢。

当心智经过总线时,会上一把锁(lock),线程1是不能去主内存中获取值的,当执行了第5步,释放了锁(unlock),线程1才能去主内存中获取。


当线程1嗅探到值有变化,会让自己工作内存中的initFlag失效。然后线程1就执行下面步骤:


1)从主内存中read新值

2)将新值load进工作内存中

3)将工作内存中的变量赋新值

4)将新值use进线程1的执行引擎中


到这里变量initFlag就实现了可见性。

 

volatile不保证原子性

注意:volatile并不能保证原子性

public class VolatileAtomicTest {    public static volatile int num = 0;
   public static void increase() {        num++;    }
   public static void main(String[] args) throws InterruptedException {        Thread[] threads = new Thread[10];        for (int i = 0; i < threads.length; i++) {            threads[i] = new Thread(() -> {                for (int j = 0; j < 1000; j++) {                    increase();                }            });
           threads[i].start();        }
       for (Thread thread : threads) {            thread.join();        }
       System.out.println(num);    }}


上面的输出会是10000么,实际输出为:9985。为什么会这样呢


原因是虽然num在线程间是可见的,但是数据从线程的工作内存同步到主内存是需要时间的,这时其他线程自己有自己的计算,导致结果不可预测。num++经历了两步:a = num + 1;num = a。这两步不能保证原子性。如果想保证原子性,需要对increase()方法加锁。


  volatile静止指令重排

public class VolatileSerialTest {
   static int x = 0, y = 0;
   public static void main(String[] args) throws InterruptedException {
       Set<String> resultSet = new HashSet<>();
       Map<String, Integer> resultMap = new HashMap<>();
       for (int i = 0; i < 1000000; i++) {            x = 0;            y = 0;            resultMap.clear();
           Thread one = new Thread(() -> {                int a = y;                x = 1;                resultMap.put("a", a);            });
           Thread other = new Thread(() -> {                int b = x;                y = 1;                resultMap.put("b", b);            });
           one.start();            other.start();
           one.join();            other.join();
           resultSet.add("a=" + resultMap.get("a") + "," + "b=" + resultMap.get("b"));            System.out.println(resultSet);        }    }}


上面代码你觉得会输出什么呢?

答案是:

[a=0,b=1][a=1,b=0][a=1,b=1]   [a=0,b=0]   //不可思意吧


第四种输出不可思议吧,按照程序逻辑。是不可能出现[a=0,b=0]的情况的。其实CPU会对代码的执行顺序进行优化,及指令重排。


指令重排可以提高CPU处理速度。

Java关键字——volatile底原理分析_java_03


Java关键字——volatile底原理分析_java_04


如果给x,y加上volatile修饰,则不会出现指令重排,[a=0,b=0]就不会出现。


我们在编写懒汉式的单列模式时,也需要给对象加volatile修饰。

public class LazySimpleSingleton {    private static volatile LazySimpleSingleton instance = null; //需要volatile修饰
   private LazySimpleSingleton(){    }
   public static LazySimpleSingleton getInstance() {        if (instance == null) {            synchronized (LazySimpleSingleton.class) {                if (instance == null) {                    instance = new LazySimpleSingleton();                }            }        }        return instance;    }}
Java关键字——volatile底原理分析_java_05


不加volatile,由于存在指令重排。线程A会出现b,c顺序颠倒情况。线程B进去获得实例可能未完全初始化。这时线程C执行方法getInstance(),返回的对象是未完全初始化的值。