volatile關鍵字及其作用
概述:本文主要介紹Java語言中的volatile關鍵字,內容涵蓋volatile的保證記憶體可見性、禁止指令重排等。
1 保證記憶體可見性
1.1 基本概念
可見性是指執行緒之間的可見性,一個執行緒修改的狀態對另一個執行緒是可見的。也就是一個執行緒修改的結果,另一個執行緒馬上就能看到。
1.2 實現原理
當對非volatile變數進行讀寫的時候,每個執行緒先從主記憶體拷貝變數到CPU快取中,如果計算機有多個CPU,每個執行緒可能在不同的CPU上被處理,這意味著每個執行緒可以拷貝到不同的CPU cache中。
volatile變數不會被快取在暫存器或者對其他處理器不可見的地方,保證了每次讀寫變數都從主記憶體中讀,跳過CPU cache這一步。當一個執行緒修改了這個變數的值,新值對於其他執行緒是立即得知的。
2 禁止指令重排
2.1 基本概念
指令重排序是JVM為了優化指令、提高程式執行效率,在不影響單執行緒程式執行結果的前提下,儘可能地提高並行度。指令重排序包括編譯器重排序和執行時重排序。
在JDK1.5之後,可以使用volatile變數禁止指令重排序。針對volatile修飾的變數,在讀寫操作指令前後會插入記憶體屏障,指令重排序時不能把後面的指令重排序到記憶體屏
示例說明:
double r = 2.1; //(1)
double pi = 3.14;//(2)
double area = pi*r*r;//(3)
雖然程式碼語句的定義順序為1->2->3,但是計算順序1->2->3與2->1->3對結果並無影響,所以編譯時和執行時可以根據需要對1、2語句進行重排序。
2.2 指令重排帶來的問題
如果一個操作不是原子的,就會給JVM留下重排的機會。
執行緒A中
{
context = loadContext();
inited = true;
}
執行緒B中
{
if (inited)
fun(context);
}
如果執行緒A中的指令發生了重排序,那麼B中很可能就會拿到一個尚未初始化或尚未初始化完成的context,從而引發程式錯誤。
2.3 禁止指令重排的原理
volatile關鍵字提供記憶體屏障的方式來防止指令被重排,編譯器在生成位元組碼檔案時,會在指令序列中插入記憶體屏障來禁止特定型別的處理器重排序。
JVM記憶體屏障插入策略:
- 每個volatile寫操作的前面插入一個StoreStore屏障;
- 在每個volatile寫操作的後面插入一個StoreLoad屏障;
- 在每個volatile讀操作的後面插入一個LoadLoad屏障;
- 在每個volatile讀操作的後面插入一個LoadStore屏障。
2.4 指令重排在雙重鎖定單例模式中的影響
基於雙重檢驗的單例模式(懶漢型)
public class Singleton3 {
private static Singleton3 instance = null;
private Singleton3() {}
public static Singleton3 getInstance() {
if (instance == null) {
synchronized(Singleton3.class) {
if (instance == null)
instance = new Singleton3();// 非原子操作
}
}
return instance;
}
}
instance= new Singleton()並不是一個原子操作,其實際上可以抽象為下面幾條JVM指令:
memory =allocate(); //1:分配物件的記憶體空間
ctorInstance(memory); //2:初始化物件
instance =memory; //3:設定instance指向剛分配的記憶體地址
上面操作2依賴於操作1,但是操作3並不依賴於操作2。所以JVM是可以針對它們進行指令的優化重排序的,經過重排序後如下:
memory =allocate(); //1:分配物件的記憶體空間
instance =memory; //3:instance指向剛分配的記憶體地址,此時物件還未初始化
ctorInstance(memory); //2:初始化物件
指令重排之後,instance指向分配好的記憶體放在了前面,而這段記憶體的初始化被排在了後面。線上程A執行這段賦值語句,在初始化分配物件之前就已經將其賦值給instance引用,恰好另一個執行緒進入方法判斷instance引用不為null,然後就將其返回使用,導致出錯。
解決辦法
用volatile關鍵字修飾instance變數,使得instance在讀、寫操作前後都會插入記憶體屏障,避免重排序。
public class Singleton3 {
private static volatile Singleton3 instance = null;
private Singleton3() {}
public static Singleton3 getInstance() {
if (instance == null) {
synchronized(Singleton3.class) {
if (instance == null)
instance = new Singleton3();
}
}
return instance;
}
}
3 適用場景
(1)volatile是輕量級同步機制。在訪問volatile變數時不會執行加鎖操作,因此也就不會使執行執行緒阻塞,是一種比synchronized關鍵字更輕量級的同步機制。
(2)volatile**無法同時保證記憶體可見性和原子性。加鎖機制既可以確保可見性又可以確保原子性,而volatile變數只能確保可見性**。
(3)volatile不能修飾寫入操作依賴當前值的變數。宣告為volatile的簡單變數如果當前值與該變數以前的值相關,那麼volatile關鍵字不起作用,也就是說如下的表示式都不是原子操作:“count++”、“count = count+1”。
(4)當要訪問的變數已在synchronized程式碼塊中,或者為常量時,沒必要使用volatile;
(5)volatile遮蔽掉了JVM中必要的程式碼優化,所以在效率上比較低,因此一定在必要時才使用此關鍵字。