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內存屏障插入策略:

  1. 每個volatile寫操作的前面插入一個StoreStore屏障;
  2. 在每個volatile寫操作的後面插入一個StoreLoad屏障;
  3. 在每個volatile讀操作的後面插入一個LoadLoad屏障;
  4. 在每個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中必要的代碼優化,所以在效率上比較低,因此一定在必要時才使用此關鍵字。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章