不論是在面試仍是實際開發中 volatile
都是一個應該掌握的技能。java
首先來看看爲何會出現這個關鍵字。git
因爲 Java
內存模型(JMM
)規定,全部的變量都存放在主內存中,而每一個線程都有着本身的工做內存(高速緩存)。github
線程在工做時,須要將主內存中的數據拷貝到工做內存中。這樣對數據的任何操做都是基於工做內存(效率提升),而且不能直接操做主內存以及其餘線程工做內存中的數據,以後再將更新以後的數據刷新到主內存中。面試
這裏所提到的主內存能夠簡單認爲是 堆內存,而工做內存則能夠認爲是 棧內存。
以下圖所示:算法
因此在併發運行時可能會出現線程 B 所讀取到的數據是線程 A 更新以前的數據。緩存
顯然這確定是會出問題的,所以 volatile
的做用出現了:安全
當一個變量被
volatile
修飾時,任何線程對它的寫操做都會當即刷新到主內存中,而且會強制讓緩存了該變量的線程中的數據清空,必須從主內存從新讀取最新數據。
volatile
修飾以後並非讓線程直接從主內存中獲取數據,依然須要將變量拷貝到工做內存中。多線程
當咱們須要在兩個線程間依據主內存通訊時,通訊的那個變量就必須的用 volatile
來修飾:併發
public class Volatile implements Runnable{ private static volatile boolean flag = true ; @Override public void run() { while (flag){ System.out.println(Thread.currentThread().getName() + "正在運行。。。"); } System.out.println(Thread.currentThread().getName() +"執行完畢"); } public static void main(String[] args) throws InterruptedException { Volatile aVolatile = new Volatile(); new Thread(aVolatile,"thread A").start(); System.out.println("main 線程正在運行") ; TimeUnit.MILLISECONDS.sleep(100) ; aVolatile.stopThread(); } private void stopThread(){ flag = false ; } }
主線程在修改了標誌位使得線程 A 當即中止,若是沒有用 volatile
修飾,就有可能出現延遲。ide
但這裏有個誤區,這樣的使用方式容易給人的感受是:
對
volatile
修飾的變量進行併發操做是線程安全的。
這裏要重點強調,volatile
並不能保證線程安全性!
以下程序:
public class VolatileInc implements Runnable{ private static volatile int count = 0 ; //使用 volatile 修飾基本數據內存不能保證原子性 //private static AtomicInteger count = new AtomicInteger() ; @Override public void run() { for (int i=0;i<10000 ;i++){ count ++ ; //count.incrementAndGet() ; } } public static void main(String[] args) throws InterruptedException { VolatileInc volatileInc = new VolatileInc() ; Thread t1 = new Thread(volatileInc,"t1") ; Thread t2 = new Thread(volatileInc,"t2") ; t1.start(); //t1.join(); t2.start(); //t2.join(); for (int i=0;i<10000 ;i++){ count ++ ; //count.incrementAndGet(); } System.out.println("最終Count="+count); } }
當咱們三個線程(t1,t2,main)同時對一個 int
進行累加時會發現最終的值都會小於 30000。
這是由於雖然volatile
保證了內存可見性,每一個線程拿到的值都是最新值,但count ++
這個操做並非原子的,這裏面涉及到獲取值、自增、賦值的操做並不能同時完成。
synchronize
或者是鎖的方式來保證原子性。Atomic
包中 AtomicInteger
來替換 int
,它利用了 CAS
算法來保證了原子性。內存可見性只是 volatile
的其中一個語義,它還能夠防止 JVM
進行指令重排優化。
舉一個僞代碼:
int a=10 ;//1 int b=20 ;//2 int c= a+b ;//3
一段特別簡單的代碼,理想狀況下它的執行順序是:1>2>3
。但有可能通過 JVM 優化以後的執行順序變爲了 2>1>3
。
能夠發現無論 JVM 怎麼優化,前提都是保證單線程中最終結果不變的狀況下進行的。
可能這裏還看不出有什麼問題,那看下一段僞代碼:
private static Map<String,String> value ; private static volatile boolean flag = fasle ; //如下方法發生在線程 A 中 初始化 Map public void initMap(){ //耗時操做 value = getMapValue() ;//1 flag = true ;//2 } //發生在線程 B中 等到 Map 初始化成功進行其餘操做 public void doSomeThing(){ while(!flag){ sleep() ; } //dosomething doSomeThing(value); }
這裏就能看出問題了,當 flag
沒有被 volatile
修飾時,JVM
對 1 和 2 進行重排,致使 value
都尚未被初始化就有可能被線程 B 使用了。
因此加上 volatile
以後能夠防止這樣的重排優化,保證業務的正確性。
一個經典的使用場景就是雙重懶加載的單例模式了:
public class Singleton { private static volatile Singleton singleton; private Singleton() { } public static Singleton getInstance() { if (singleton == null) { synchronized (Singleton.class) { if (singleton == null) { //防止指令重排 singleton = new Singleton(); } } } return singleton; } }
這裏的 volatile
關鍵字主要是爲了防止指令重排。
若是不用 ,singleton = new Singleton();
,這段代碼實際上是分爲三步:
singleton
對象指向分配的內存地址。(3)加上 volatile
是爲了讓以上的三步操做順序執行,反之有可能第二步在第三步以前被執行就有可能某個線程拿到的單例對象是尚未初始化的,以至於報錯。
volatile
在 Java
併發中用的不少,好比像 Atomic
包中的 value
、以及 AbstractQueuedLongSynchronizer
中的 state
都是被定義爲 volatile
來用於保證內存可見性。
將這塊理解透徹對咱們編寫併發程序時能夠提供很大幫助。
最近在總結一些 Java 相關的知識點,感興趣的朋友能夠一塊兒維護。
地址: https://github.com/crossoverJie/Java-Interview