內存可見性:通俗來講就是,線程A對一個volatile變量的修改,對於其它線程來講是可見的,即線程每次獲取volatile變量的值都是最新的。java
經過關鍵字sychronize能夠防止多個線程進入同一段代碼,在某些特定場景中,volatile至關於一個輕量級的sychronize,由於不會引發線程的上下文切換,可是使用volatile必須知足兩個條件:
一、對變量的寫操做不依賴當前值,如多線程下執行a++,是沒法經過volatile保證結果準確性的;
二、該變量沒有包含在具備其它變量的不變式中,這句話有點拗口,看代碼比較直觀。緩存
public class NumberRange { private volatile int lower = 0; private volatile int upper = 10; public int getLower() { return lower; } public int getUpper() { return upper; } public void setLower(int value) { if (value > upper) throw new IllegalArgumentException(...); lower = value; } public void setUpper(int value) { if (value < lower) throw new IllegalArgumentException(...); upper = value; } }
上述代碼中,上下界初始化分別爲0和10,假設線程A和B在某一時刻同時執行了setLower(8)和setUpper(5),且都經過了不變式的檢查,設置了一個無效範圍(8, 5),因此在這種場景下,須要經過sychronize保證方法setLower和setUpper在每一時刻只有一個線程可以執行。bash
下面是咱們在項目中常常會用到volatile關鍵字的兩個場景:多線程
一、狀態標記量
在高併發的場景中,經過一個boolean類型的變量isopen,控制代碼是否走促銷邏輯,該如何實現?併發
public class ServerHandler { private volatile isopen; public void run() { if (isopen) { //促銷邏輯 } else { //正常邏輯 } } public void setIsopen(boolean isopen) { this.isopen = isopen } }
場景細節無需過度糾結,這裏只是舉個例子說明volatile的使用方法,用戶的請求線程執行run方法,若是須要開啓促銷活動,能夠經過後臺設置,具體實現能夠發送一個請求,調用setIsopen方法並設置isopen爲true,因爲isopen是volatile修飾的,因此一經修改,其餘線程均可以拿到isopen的最新值,用戶請求就能夠執行促銷邏輯了。高併發
二、double check
單例模式的一種實現方式,但不少人會忽略volatile關鍵字,由於沒有該關鍵字,程序也能夠很好的運行,只不過代碼的穩定性總不是100%,說不定在將來的某個時刻,隱藏的bug就出來了。性能
class Singleton { private volatile static Singleton instance; public static Singleton getInstance() { if (instance == null) { syschronized(Singleton.class) { if (instance == null) { instance = new Singleton(); } } } return instance; } }
不過在衆多單例模式的實現中,我比較推薦懶加載的優雅寫法Initialization on Demand Holder(IODH)。this
public class Singleton { static class SingletonHolder { static Singleton instance = new Singleton(); } public static Singleton getInstance(){ return SingletonHolder.instance; } }
固然,若是不須要懶加載的話,直接初始化的效果更好。spa
在java虛擬機的內存模型中,有主內存和工做內存的概念,每一個線程對應一個工做內存,並共享主內存的數據,下面看看操做普通變量和volatile變量有什麼不一樣:線程
一、對於普通變量:讀操做會優先讀取工做內存的數據,若是工做內存中不存在,則從主內存中拷貝一份數據到工做內存中;寫操做只會修改工做內存的副本數據,這種狀況下,其它線程就沒法讀取變量的最新值。
二、對於volatile變量,讀操做時JMM會把工做內存中對應的值設爲無效,要求線程從主內存中讀取數據;寫操做時JMM會把工做內存中對應的數據刷新到主內存中,這種狀況下,其它線程就能夠讀取變量的最新值。
volatile變量的內存可見性是基於內存屏障(Memory Barrier)實現的,什麼是內存屏障?內存屏障,又稱內存柵欄,是一個CPU指令。在程序運行時,爲了提升執行性能,編譯器和處理器會對指令進行重排序,JMM爲了保證在不一樣的編譯器和CPU上有相同的結果,經過插入特定類型的內存屏障來禁止特定類型的編譯器重排序和處理器重排序,插入一條內存屏障會告訴編譯器和CPU:無論什麼指令都不能和這條Memory Barrier指令重排序。
這段文字顯得有點蒼白無力,不如來段簡明的代碼:
class Singleton { private volatile static Singleton instance; private int a; private int b; private int b; public static Singleton getInstance() { if (instance == null) { syschronized(Singleton.class) { if (instance == null) { a = 1; // 1 b = 2; // 2 instance = new Singleton(); // 3 c = a + b; // 4 } } } return instance; } }
一、若是變量instance沒有volatile修飾,語句一、二、3能夠隨意的進行重排序執行,即指令執行過程多是3214或1324。
二、若是是volatile修飾的變量instance,會在語句3的先後各插入一個內存屏障。
經過觀察volatile變量和普通變量所生成的彙編代碼能夠發現,操做volatile變量會多出一個lock前綴指令:
Java代碼:
instance = new Singleton();
彙編代碼:
0x01a3de1d: movb $0x0,0x1104800(%esi); 0x01a3de24: **lock** addl $0x0,(%esp);
這個lock前綴指令至關於上述的內存屏障,提供瞭如下保證:
一、將當前CPU緩存行的數據寫回到主內存;
二、這個寫回內存的操做會致使在其它CPU裏緩存了該內存地址的數據無效。
CPU爲了提升處理性能,並不直接和內存進行通訊,而是將內存的數據讀取到內部緩存(L1,L2)再進行操做,但操做完並不能肯定什麼時候寫回到內存,若是對volatile變量進行寫操做,當CPU執行到Lock前綴指令時,會將這個變量所在緩存行的數據寫回到內存,不過仍是存在一個問題,就算內存的數據是最新的,其它CPU緩存的仍是舊值,因此爲了保證各個CPU的緩存一致性,每一個CPU經過嗅探在總線上傳播的數據來檢查本身緩存的數據有效性,當發現本身緩存行對應的內存地址的數據被修改,就會將該緩存行設置成無效狀態,當CPU讀取該變量時,發現所在的緩存行被設置爲無效,就會從新從內存中讀取數據到緩存中。