前言面试
不论是在面试仍是实际开发中 volatile 都是一个应该掌握的技能。算法
首先来看看为何会出现这个关键字。缓存
内存可见性安全
因为 Java 内存模型(JMM)规定,全部的变量都存放在主内存中,而每一个线程都有着本身的工做内存(高速缓存)。多线程
线程在工做时,须要将主内存中的数据拷贝到工做内存中。这样对数据的任何操做都是基于工做内存(效率提升),而且不能直接操做主内存以及其余线程工做内存中的数据,以后再将更新以后的数据刷新到主内存中。并发
这里所提到的主内存能够简单认为是堆内存,而工做内存则能够认为是栈内存。ide
以下图所示:优化
因此在并发运行时可能会出现线程 B 所读取到的数据是线程 A 更新以前的数据。spa
显然这确定是会出问题的,所以 volatile 的做用出现了:线程
当一个变量被 volatile 修饰时,任何线程对它的写操做都会当即刷新到主内存中,而且会强制让缓存了该变量的线程中的数据清空,必须从主内存从新读取最新数据。
volatile 修饰以后并非让线程直接从主内存中获取数据,依然须要将变量拷贝到工做内存中。
内存可见性的应用
当咱们须要在两个线程间依据主内存通讯时,通讯的那个变量就必须的用 volatile 来修饰:
1 public class Volatile implements Runnable{ 2 3 private static volatile boolean flag = true ; 4 5 @Override 6 public void run() { 7 while (flag){ 8 } 9 System.out.println(Thread.currentThread().getName() +"执行完毕"); 10 } 11 12 public static void main(String[] args) throws InterruptedException { 13 Volatile aVolatile = new Volatile(); 14 new Thread(aVolatile,"thread A").start(); 15 16 17 System.out.println("main 线程正在运行") ; 18 19 Scanner sc = new Scanner(System.in); 20 while(sc.hasNext()){ 21 String value = sc.next(); 22 if(value.equals("1")){ 23 24 new Thread(new Runnable() { 25 @Override 26 public void run() { 27 aVolatile.stopThread(); 28 } 29 }).start(); 30 31 break ; 32 } 33 } 34 35 System.out.println("主线程退出了!"); 36 37 } 38 39 private void stopThread(){ 40 flag = false ; 41 } 42 43 }
主线程在修改了标志位使得线程 A 当即中止,若是没有用 volatile 修饰,就有可能出现延迟。
但这里有个误区,这样的使用方式容易给人的感受是:
对 volatile 修饰的变量进行并发操做是线程安全的。
这里要重点强调,volatile 并不能保证线程安全性!
以下程序:
1 public class VolatileInc implements Runnable{ 2 3 private static volatile int count = 0 ; //使用 volatile 修饰基本数据内存不能保证原子性 4 5 //private static AtomicInteger count = new AtomicInteger() ; 6 7 @Override 8 public void run() { 9 for (int i=0;i<10000 ;i++){ 10 count ++ ; 11 //count.incrementAndGet() ; 12 } 13 } 14 15 public static void main(String[] args) throws InterruptedException { 16 VolatileInc volatileInc = new VolatileInc() ; 17 Thread t1 = new Thread(volatileInc,"t1") ; 18 Thread t2 = new Thread(volatileInc,"t2") ; 19 t1.start(); 20 //t1.join(); 21 22 t2.start(); 23 //t2.join(); 24 for (int i=0;i<10000 ;i++){ 25 count ++ ; 26 //count.incrementAndGet(); 27 } 28 29 30 System.out.println("最终Count="+count); 31 } 32 }
当咱们三个线程(t1,t2,main)同时对一个 int 进行累加时会发现最终的值都会小于 30000。
这是由于虽然 volatile 保证了内存可见性,每一个线程拿到的值都是最新值,但 count ++ 这个操做并非原子的,这里面涉及到获取值、自增、赋值的操做并不能同时完成。
因此想到达到线程安全可使这三个线程串行执行(其实就是单线程,没有发挥多线程的优点)。
也可使用 synchronize 或者是锁的方式来保证原子性。
还能够用 Atomic 包中 AtomicInteger 来替换 int,它利用了 CAS 算法来保证了原子性。
指令重排
内存可见性只是 volatile 的其中一个语义,它还能够防止 JVM 进行指令重排优化。
举一个伪代码:
1 int a=10 ;//1 2 int b=20 ;//2 3 int c= a+b ;//3
一段特别简单的代码,理想状况下它的执行顺序是:1>2>3。但有可能通过 JVM 优化以后的执行顺序变为了 2>1>3。
能够发现无论 JVM 怎么优化,前提都是保证单线程中最终结果不变的状况下进行的。
可能这里还看不出有什么问题,那看下一段伪代码:
1 private static Map<String,String> value ; 2 private static volatile boolean flag = fasle ; 3 4 //如下方法发生在线程 A 中 初始化 Map 5 public void initMap(){ 6 //耗时操做 7 value = getMapValue() ;//1 8 flag = true ;//2 9 } 10 11 12 //发生在线程 B中 等到 Map 初始化成功进行其余操做 13 public void doSomeThing(){ 14 while(!flag){ 15 sleep() ; 16 } 17 //dosomething 18 doSomeThing(value); 19 }
这里就能看出问题了,当 flag 没有被 volatile 修饰时,JVM 对 1 和 2 进行重排,致使 value 都尚未被初始化就有可能被线程 B 使用了。
因此加上 volatile 以后能够防止这样的重排优化,保证业务的正确性。
指令重排的的应用
一个经典的使用场景就是双重懒加载的单例模式了:
1 public class Singleton { 2 3 private static volatile Singleton singleton; 4 5 private Singleton() { 6 } 7 8 public static Singleton getInstance() { 9 if (singleton == null) { 10 synchronized (Singleton.class) { 11 if (singleton == null) { 12 //防止指令重排 13 singleton = new Singleton(); 14 } 15 } 16 } 17 return singleton; 18 } 19 }
这里的 volatile 关键字主要是为了防止指令重排。
若是不用 ,singleton = new Singleton();,这段代码实际上是分为三步:
分配内存空间。(1)
初始化对象。(2)
将 singleton 对象指向分配的内存地址。(3)
加上 volatile 是为了让以上的三步操做顺序执行,反之有可能第二步在第三步以前被执行就有可能某个线程拿到的单例对象是尚未初始化的,以至于报错。
总结
volatile 在 Java 并发中用的不少,好比像 Atomic 包中的 value、以及 AbstractQueuedLongSynchronizer 中的 state 都是被定义为 volatile 来用于保证内存可见性。
将这块理解透彻对咱们编写并发程序时能够提供很大帮助。
参考:https://crossoverjie.top/2018/03/09/volatile/