一、簡介
volatile能保證可見性、有序性。
synchronize與volatile的差別就是,synchronize能保證原子性,而volatile不能。
特殊情況下可以保證原子性(比如long,64位,先讀前32位再讀後32位,如果這個long變量用volatile修飾就能保證原子性)。
它靠記憶體屏障和禁止重排序來實作可見性、有序性。
二、可見性
1.導緻共享變量線上程間不可見的原因:
- 線程交叉執行
- 重排序結合線程交叉執行
- 共享變量更新後的值沒有線上程工作記憶體和主存之間及時更新
2.可見性(synchronize、final、volatile可以保證可見性)
這篇隻說volatile,synchronize在其他篇章聊。
可見性是說:一個線程對main memory的修改可以及時的被其他線程觀察到。
被volatile修飾的變量能夠保證每個線程能夠擷取該變量的最新值,進而避免出現資料髒讀的現象。
java記憶體模型
volatile修飾的共享變量保證可見性和有序性的原因:
1.它将目前cpu cache memory行的資料寫回main memory
2.這個寫回main memory的操作會使得其他CPU裡緩存了該記憶體位址的資料無效
3.其他線程用到這個變量的時候會直接從main memory中讀取,而不是使用cpu cache memory中的備份。
而普通的共享變量不能保證可見性,因為普通共享變量被修改之後,什麼時候被寫入主存是不确定的,當其他線程去讀取時,此時記憶體中可能還是原來的舊值,是以無法保證可見性。
三、有序性(synchronize和volatile可以保證有序性)
有序性:一個線程觀察其他線程中的指令執行順序,由于指令重排序的存在,該觀察結果一般是雜亂無序的。
在Java記憶體模型中,允許編譯器和處理器對指令進行重排序,重排序過程不會影響到單線程程式的執行,但會影響到多線程并發執行的正确性。
java記憶體模型的先天有序性happens-before原則
JSR-133中定義了如下的 happens-before 規則:
- 單一線程原則:在一個線程内,程式前面的操作先于後面的操作。
- 螢幕鎖規則:一個unlock操作先于後面對同一個鎖的lock操作發生。
- volatile變量規則:對一個 volatile 變量的寫操作先行發生于後面對這個變量的讀操作,也就是說讀取的值肯定是最新的。
- 線程啟動規則:Thread對象的start()方法調用先行發生于此線程的每一個動作。
- 線程加入規則:Thread 對象的結束先行發生于 join() 方法傳回。
- 線程中斷規則:對線程 interrupt() 方法的調用先行發生于被中斷線程的代碼檢測到中斷事件的發生,可以通過 interrupted() 方法檢測到是否有中斷發生。
- 對象終結規則:一個對象的初始化完成(構造函數執行結束)先行發生于它的 finalize() 方法的開始。
- 傳遞性:如果操作 A 先行發生于操作 B,操作 B 先行發生于操作 C,那麼操作 A 先行發生于操作 C。
如果兩個操作執行順序無法從happens-before原則推導出來,那麼他們就不能保證有序性,jvm可以随意的對他們進行重排序。
四、記憶體屏障和禁止重排序
JMM四類記憶體屏障
屏障類型 | 指令示例 | 說明 |
LoadLoad Barriers | Load1;LoadLoad;Load2 | 確定Load1資料的裝載先于Load2及所有後續裝載指令 |
StoreStore Barriers | Store1;StoreStore;Store2 | 確定Store1資料重新整理到記憶體,先于Store2及所有後續存儲指令 |
LoadStore Barriers | Load1;LoadStore;Store2 | 確定Load1資料裝載先Store2及所有後續存儲指令 |
StoreLoad Barriers | Store1;StoreLoad;Load2 | 確定Store1資料重新整理到記憶體,先于Load2及所有後續裝載。StoreLoad Barriers會使該屏障之前的所有記憶體通路指令(存儲和裝載)完成之後,才執行該屏障之後的記憶體通路指令。 |
JMM會針對編譯器制定volatile重排序規則
是否重排序 | 第二步 | ||
第一步 | 普通讀/寫 | volatile讀 | volatile寫 |
普通讀/寫 | NO | ||
volatile讀 | NO | NO | NO |
volatile寫 | NO | NO |
編譯器在生成位元組碼時,會在指令序列中插入記憶體屏障來禁止特定類型的處理器重排序
- 在每個volatile寫操作的前面插入一個StoreStore屏障;
- 在每個volatile寫操作的後面插入一個StoreLoad屏障;
- 在每個volatile讀操作的後面插入一個LoadLoad屏障;
- 在每個volatile讀操作的後面插入一個LoadStore屏障。
volatile寫插入屏障示例:
volatile讀插入屏障示例:
五、常用場景
1.狀态标記量
volatile boolean inited = false;
//線程1:
context = loadContext();
inited = true;
//線程2:
while(!inited ){
sleep()
}
doSomethingwithconfig(context);
因為context=loadContext()和inited=true之間的執行順序不能保證(不符合happens-before中的任何一條),是以inited變量要用volatile修飾,以免出現這種情況:
線程1在context=loadContext()方法執行之前就先執行了inited=true,此時context根本沒有加載
而,線程2恰好在此時判斷出inited=true,就去執行doSomethingwithconfig(context), 此時context根本沒有加載
2.雙重檢查
/**
* 懶漢模式 -》 雙重同步鎖單例模式
* 單例執行個體在第一次使用時進行建立
*/
public class SingletonExample4 {
// 私有構造函數
private SingletonExample4() {
}
// 1、memory = allocate() 配置設定對象的記憶體空間
// 2、ctorInstance() 初始化對象
// 3、instance = memory 設定instance指向剛配置設定的記憶體
// JVM和cpu優化,發生了指令重排
// 1、memory = allocate() 配置設定對象的記憶體空間
// 3、instance = memory 設定instance指向剛配置設定的記憶體
// 2、ctorInstance() 初始化對象
// 單例對象 volatile + 雙重檢測機制 -> 禁止指令重排
private volatile static SingletonExample4 instance = null;
// 靜态的工廠方法
public static SingletonExample4 getInstance() {
if (instance == null) { // 雙重檢測機制 // B
synchronized (SingletonExample4.class) { // 同步鎖
if (instance == null) {
instance = new SingletonExample4(); // A - 3
}
}
}
return instance;
}
}