1. 程式人生 > >volatile關鍵字及其作用

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中必要的程式碼優化,所以在效率上比較低,因此一定在必要時才使用此關鍵字。