volatile 是一个类型修饰符。volatile 的作用是作为指令关键字,确保本条指令不会因编译器的优化而省略,在一定情况下可以保证线程间的同步性。本文介绍了volatile的特性、实现机制以及使用场景

一、volatile特性

1.1 volatile可见性

一个线程修改了某个共享变量的值,这新值对其他线程来说是立即可见的。

当写一个volatile共享变量时,JMM会把该线程本地内存中的共享变量值立即刷新到主内存中,同时JMM会把其他线程本地内存中缓存该变量的缓存行置为无效,从而使其他线程只能去主存中读取最新的值。

例如以下例子,NonVisibilityDemo 中的示例包含两个共享数据的线程。写线程将更新标志,读线程将等待直到设置标志。如果这个标志flag没有被volatile修饰,则可能会一直循环下去,因为读线程可能读取不到写线程对于 flag 的写入而永远等待。

public class NonVisibilityDemo {
    public static void main(String[] args) throws InterruptedException {
        new ReadThread().start();
        Thread.sleep(1000);
        new WriteThread().start();
    }
}


class ReadThread extends Thread {

    @Override
    public void run() {
        System.out.println("read-thread start");
        while (true) {
            if (ShareData.flag == 1) {
                System.out.println("read-thread end");
                break;
            }
        }
    }
}

class ShareData {
    public volatile static int flag = -1;
}


class WriteThread extends Thread {
    @Override
    public void run() {
        ShareData.flag = 1;
        System.out.println("write-thread:flag=" + ShareData.flag);
    }
}

1.2 volatile有序性

对于volatile修饰的变量,编译器生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。

volatile重排序规则

  • 当第二个操作为volatile写操做时,不管第一个操作是什么(普通读写或者volatile读写),都不能进行重排序。这个规则确保volatile写之前的所有操作都不会被重排序到volatile之后;
  • 当第一个操作为volatile读操作时,不管第二个操作是什么,都不能进行重排序。这个规则确保volatile读之后的所有操作都不会被重排序到volatile之前;
  • 当第一个操作是volatile写操作时,第二个操作是volatile读操作,不能进行重排序。这个规则和前面两个规则一起构成了:两个volatile变量操作不能够进行重排序;

除以上三种情况以外可以进行重排序。

1.3 volatile不保证原子性

public class Test {
    public volatile int inc = 0;
     
    public void increase() {
        inc++;
    }     
    public static void main(String[] args) {
        final Test test = new Test();
        for(int i=0;i<10;i++){
            new Thread(){
                public void run() {
                    for(int j=0;j<1000;j++)
                        test.increase();
                };
            }.start();
        }    
        //后台默认两个线程:一个是main线程,一个是gc线程
        while(Thread.activeCount()>2)
            Thread.yield();
        System.out.println(test.inc);
    }
}

执行以上程序,运行结果总是小于等于10000的一个随机数。这是因为i++是一个复合操作,这个操作包含了以下三个步骤:

  1. 线程读取i
  2. temp = i + 1
  3. i = temp

假设当 i=5 的时候A,B两个线程同时读入了 i 的值, 然后A线程执行了 temp = i + 1的操作, 要注意,此时的 i 的值还没有变化,然后B线程也执行了 temp = i + 1的操作,注意,此时A,B两个线程保存的 i 的值都是5,temp 的值都是6, 然后A线程执行了 i = temp (6)的操作,此时i的值会立即刷新到主存并通知其他线程保存的 i 值失效, 此时B线程需要重新读取 i 的值那么此时B线程保存的 i 就是6,同时B线程保存的 temp 还仍然是6, 然后B线程执行 i=temp (6),所以导致了计算结果比预期少了1。

二、volatile的实现机制

观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令。lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障是一组处理器指令,用于实现对内存操作的顺序限制。

2.1 内存屏障的三个功能:

  • 它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
  • 它会强制将对缓存的修改操作立即写入主存;
  • 如果是写操作,它会导致其他CPU中对应的缓存行无效。

2.2 内存屏障插入策略

先简单了解两个指令:

  • Store:将处理器缓存的数据刷新到内存中。
  • Load:将内存存储的数据拷贝到处理器的缓存中。
屏障类型指令示例说明
LoadLoad BarriersLoad1;LoadLoad;Load2该屏障确保Load1数据的装载先于Load2及其后所有装载指令的的操作
StoreStore BarriersStore1;StoreStore;Store2该屏障确保Store1立刻刷新数据到内存(使其对其他处理器可见)的操作先于Store2及其后所有存储指令的操作
LoadStore BarriersLoad1;LoadStore;Store2确保Load1的数据装载先于Store2及其后所有的存储指令刷新数据到内存的操作
StoreLoad BarriersStore1;StoreLoad;Load2该屏障确保Store1立刻刷新数据到内存的操作先于Load2及其后所有装载装载指令的操作。它会使该屏障之前的所有内存访问指令(存储指令和访问指令)完成之后,才执行该屏障之后的内存访问指令

基于保守策略的JMM内存屏障插入策略:

  • 在每个volatile写操作之前插入一个StoreStore屏障。保证之前的都能刷新到主存
  • 在每个volatile写操作之后插入一个StoreLoad屏障。保证先写后读,能提高效率
  • 在每个volatile读操作之后插入一个LoadLoad屏障
  • 在每个volatile读操作之后插入一个LoadStore屏障

实际执行时,只要不改变volatile写-读的内存语义,编译器可以根据具体情况省略不必要的屏障。

x86处理器只会对写读作重排序,故只有一个屏障StoreLoad即可实现volatile写-读的内存语义。

三、使用场景

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

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

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

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

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

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

3.1状态量标记

public class NonVisibilityDemo {
   public static void main(String[] args) throws InterruptedException {
       new ReadThread().start();
       Thread.sleep(1000);
       new WriteThread().start();
   }
}


class ReadThread extends Thread {
   @Override
   public void run() {
       System.out.println("read-thread start");
       while (true) {
           if (ShareData.flag == 1) {
               System.out.println("read-thread end");
               break;
           }
       }
   }
}

class ShareData {
   public static volatile int flag = -1;
}

class WriteThread extends Thread {
   @Override
   public void run() {
       ShareData.flag = 1;
       System.out.println("write-thread:flag=" + ShareData.flag);
   }
}

3.1 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;
    }
}

关于双重检查可参考:https://blog.csdn.net/chenchaofuck1/article/details/51702129/

参考资料

https://www.jianshu.com/p/64240319ed60

https://www.cnblogs.com/haimishasha/p/10978848.html

https://www.cnblogs.com/dolphin0520/p/3920373.html