1. 程式人生 > 程式設計 >Java關鍵字之volatile

Java關鍵字之volatile

是什麼

首先,volatile是什麼?他是Java提供的一個內建的關鍵字。被此關鍵字修飾的變數有兩種特性

  • 變數對所有的執行緒是可見的。即有執行緒A和B,存在被volatile修飾的關鍵字temp,當執行緒A對temp進行修改之後,修改之後的值在對執行緒B而言,是可見的,即執行緒B中獲取到的值是最新的。
  • 被volatile修飾的變數還可以避免指令重排序。在編譯之後,會為該變數建立一個記憶體屏障,即記憶體屏障後面的程式碼不能提前到記憶體屏障前面執行。 這裡需要先弄懂兩個知識點:JMM記憶體結構和指令重排序

JMM記憶體模型

請注意,JMM記憶體結構並不是指JVM虛擬的記憶體結構,及堆疊等。

JMM.png
(畫的有點醜,將就看一下)
每次執行緒讀取資料的時候,並不是直接去主記憶體中讀取,會先在工作記憶體中去查詢,變數是否存在,存在的話就直接使用,如果不存在才會去主記憶體中查詢。在Java的記憶體模型中定義了一下八種操作來完成變數從主存拷貝到工作記憶體,從工作記憶體寫回到主存。

  • lock:作用於主記憶體的變數,他把一個變數標識為執行緒獨佔狀態
  • unlock:作用於主記憶體的變數,把一個處於鎖定狀態的變數釋放出來,釋放後的變數才可以被其他執行緒鎖定
  • read:作用於主記憶體,把一個變數的值從主記憶體傳輸到工作記憶體,以便隨後的load使用
  • load:作用於工作記憶體,把read操作從主記憶體得到的變數值放入到工作記憶體變數副本中
  • use:作用於工作記憶體,把工作記憶體的一個變數的值傳遞給一個執行引擎,每當虛擬機器器遇到一個需要使用
  • assign:賦值,作用於工作記憶體的變數。他把一個從執行引擎接收到的值賦值給工作記憶體的變數
  • store:作用於工作記憶體的變數,他把工作記憶體中的一個變數的值傳送到主記憶體中
  • write:作用於主記憶體中,把store操作從工作記憶體中獲取到的值寫入到主記憶體

Java虛擬機器器對volatile關鍵字有一些特殊的規則。

  • 只有當執行緒對變數執行的前一個動作是load的時候,執行緒才能對變數執行use操作。並且只有當執行緒對變數的吼一個操作是use的時候,執行緒才能只能load操作。(總的理解就是load和use之間必須連續出現,中間不允許插入其他的指令,這個規則要求在工作記憶體中,每次使用變數前,都需要先去主存中查詢,重新整理變數的值)
  • 只有當執行緒對變數的執行的前一個動作是assign的時候,執行緒才能對變數執行store,只有當執行緒的後一個動作是store的時候,才能對變數執行assign。(其實與上一條類似,只不過這個是正對與寫,上一個是針對讀)
  • 假設動作A和B均是對變數執行use操作,P和Q都是對變數執行load操作(A和P是一組操作,B和Q是一組操作)。如果存在有A比B先執行,那麼一P比Q先執行。這個可以實現下面提到的防止指令重排序。

指令重排序

現代的作業系統為了對程式的執行進行優化,會在不改變程式執行的結果下,改變執行緒內部程式碼的執行順序。例如以下程式碼

int i = 1;
int j = 2;
int z = 3;
int x = i + j;
複製程式碼

在類似於這種情況下,如果根據程式程式碼的順序,i會先儲存,然後再讀取,在進行運算。指令重排序後,可能會是i和j在z之後定義,這樣可以減去一次載入變數的過程,會在一定程度上提高效率(當然,這只是一個簡單的例子,還有很多更復雜的情況)

為什麼

為什麼需要這麼一個關鍵字呢?當使用多執行緒的時候,難免會出現執行緒安全問題,即A執行緒修改的變數的值,B執行緒中使用該變數的時候,獲取到的值並不是最新的,導致B執行緒處理的資料成為髒資料,導致一些未知的情況。
可能有的朋友會說為什麼不使用鎖呢。可以說關鍵字volatile是Java虛擬機器器提供的最輕量的一個同步機制相對於synchronized而言,是更輕一點的鎖。synchronized在只有一個執行緒的時候,是輕量級鎖,當出現多個執行緒同時競爭的時候,會變成重量級鎖,會消耗較多的系統資源。

怎麼做

我們以一個單例模式的程式碼來舉例說明。

public class Singleton {

    private volatile static Singleton singleton;

    private Singleton() {

    }

    public static Singleton getInstance() {
        if (singleton == null) {
            synchronized (Singleton.class) {
                if (singleton == null) {
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }

    public static void main(String[] args) {
        Singleton.getInstance();
    }

}
複製程式碼

這是一個很簡單的懶漢式單例模式。當多個執行緒同時去獲取的時候,只有一個執行緒能夠建立這個物件。如果我們不新增volatile,則線上程建立好一個物件,釋放鎖之後,同時在等待獲取鎖的物件就會獲取鎖,然後判斷,發現已經建立,然後再釋放,其他競爭執行緒也是類似的。(這裡的競爭執行緒是指與建立物件的那個執行緒同時競爭鎖的執行緒,由於獲取鎖失敗,會等待)。如果添加了volatile,會在使用的時候重新去載入這個物件,發現已經初始化,則放棄獲取鎖,減少了鎖的競爭。

有什麼問題

在學習了Java的記憶體模型之後,我們發現其實Java記憶體模型就是在圍繞著併發過程中如何處理原子性、可見性和有序性。那volatile都可以保證這些特性嗎?

  • 原子性:原子性是指一次操作不能再細分。Java記憶體模型中的read、load、assign、use、store和write都可以保證原子性。那volatile可以保證原子性嗎?答案是不可以。例如以下程式碼
volatile int i = 1;
i++;
複製程式碼

對於這個來講,是很常見的一個例子,由於i++他不是一個原子性操作,他會先讀取,然後再運算,最後賦值。如果他想要實現原子性操作的話,需要藉助於其他的鎖機制。

  • 可見性:可見性是指當一個執行緒修改了共享變數的值之後,其他執行緒能夠立即得知這個修改。上文也講解到了volatile是可以支援可見性的。
  • 有序性:即是否與程式碼定義的順序一致(說的比較書面)。之前也提到volatile可以實現指令重排,保證執行緒執行的有序性。

使用條件來保證原子性

  • 運算結果並不依賴變數當前的值,或者確保只有一個單一的執行緒修改變數的值
  • 變數不需要與其他狀態變數參與不變約束