前言面試
不論是在面試仍是實際開發中 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/