概述:本文主要介紹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中必要的代碼優化,所以在效率上比較低,因此一定在必要時才使用此關鍵字。