1. 程式人生 > >Java中Volatile關鍵字詳解

Java中Volatile關鍵字詳解

一、基本概念

先補充一下概念:Java 記憶體模型中的可見性、原子性和有序性。

可見性:

  可見性是一種複雜的屬性,因為可見性中的錯誤總是會違揹我們的直覺。通常,我們無法確保執行讀操作的執行緒能適時地看到其他執行緒寫入的值,有時甚至是根本不可能的事情。為了確保多個執行緒之間對記憶體寫入操作的可見性,必須使用同步機制。

  可見性,是指執行緒之間的可見性,一個執行緒修改的狀態對另一個執行緒是可見的。也就是一個執行緒修改的結果。另一個執行緒馬上就能看到。比如:用volatile修飾的變數,就會具有可見性。volatile修飾的變數不允許執行緒內部快取和重排序,即直接修改記憶體。所以對其他執行緒是可見的。但是這裡需要注意一個問題,volatile只能讓被他修飾內容具有可見性,但不能保證它具有原子性。比如 volatile int a = 0;之後有一個操作 a++;這個變數a具有可見性,但是a++ 依然是一個非原子操作,也就是這個操作同樣存線上程安全問題。

  在 Java 中 volatile、synchronized 和 final 實現可見性。

原子性:

  原子是世界上的最小單位,具有不可分割性。比如 a=0;(a非long和double型別) 這個操作是不可分割的,那麼我們說這個操作時原子操作。再比如:a++; 這個操作實際是a = a + 1;是可分割的,所以他不是一個原子操作。非原子操作都會存線上程安全問題,需要我們使用同步技術(sychronized)來讓它變成一個原子操作。一個操作是原子操作,那麼我們稱它具有原子性。java的concurrent包下提供了一些原子類,我們可以通過閱讀API來了解這些原子類的用法。比如:AtomicInteger、AtomicLong、AtomicReference等。

  在 Java 中 synchronized 和在 lock、unlock 中操作保證原子性。

有序性:

Java 語言提供了 volatile 和 synchronized 兩個關鍵字來保證執行緒之間操作的有序性,volatile 是因為其本身包含“禁止指令重排序”的語義,synchronized 是由“一個變數在同一個時刻只允許一條執行緒對其進行 lock 操作”這條規則獲得的,此規則決定了持有同一個物件鎖的兩個同步塊只能序列執行。

下面內容摘錄自《Java Concurrency in Practice》:

  下面一段程式碼在多執行緒環境下,將存在問題。

 1 /**
2 * @author zhengbinMac 3 */ 4 public class NoVisibility { 5 private static boolean ready; 6 private static int number; 7 private static class ReaderThread extends Thread { 8 @Override 9 public void run() { 10 while(!ready) { 11 Thread.yield(); 12 } 13 System.out.println(number); 14 } 15 } 16 public static void main(String[] args) { 17 new ReaderThread().start(); 18 number = 42; 19 ready = true; 20 } 21 }
複製程式碼

  NoVisibility可能會持續迴圈下去,因為讀執行緒可能永遠都看不到ready的值。甚至NoVisibility可能會輸出0,因為讀執行緒可能看到了寫入ready的值,但卻沒有看到之後寫入number的值,這種現象被稱為“重排序”。只要在某個執行緒中無法檢測到重排序情況(即使在其他執行緒中可以明顯地看到該執行緒中的重排序),那麼就無法確保執行緒中的操作將按照程式中指定的順序來執行。當主執行緒首先寫入number,然後在沒有同步的情況下寫入ready,那麼讀執行緒看到的順序可能與寫入的順序完全相反。

  在沒有同步的情況下,編譯器、處理器以及執行時等都可能對操作的執行順序進行一些意想不到的調整。在缺乏足夠同步的多執行緒程式中,要想對記憶體操作的執行春旭進行判斷,無法得到正確的結論。

  這個看上去像是一個失敗的設計,但卻能使JVM充分地利用現代多核處理器的強大效能。例如,在缺少同步的情況下,Java記憶體模型允許編譯器對操作順序進行重排序,並將數值快取在暫存器中。此外,它還允許CPU對操作順序進行重排序,並將數值快取在處理器特定的快取中。

二、Volatile原理

  Java語言提供了一種稍弱的同步機制,即volatile變數,用來確保將變數的更新操作通知到其他執行緒。當把變數宣告為volatile型別後,編譯器與執行時都會注意到這個變數是共享的,因此不會將該變數上的操作與其他記憶體操作一起重排序。volatile變數不會被快取在暫存器或者對其他處理器不可見的地方,因此在讀取volatile型別的變數時總會返回最新寫入的值。

  在訪問volatile變數時不會執行加鎖操作,因此也就不會使執行執行緒阻塞,因此volatile變數是一種比sychronized關鍵字更輕量級的同步機制。

  當對非 volatile 變數進行讀寫的時候,每個執行緒先從記憶體拷貝變數到CPU快取中。如果計算機有多個CPU,每個執行緒可能在不同的CPU上被處理,這意味著每個執行緒可以拷貝到不同的 CPU cache 中。

  而宣告變數是 volatile 的,JVM 保證了每次讀變數都從記憶體中讀,跳過 CPU cache 這一步。

當一個變數定義為 volatile 之後,將具備兩種特性:

  1.保證此變數對所有的執行緒的可見性,這裡的“可見性”,如本文開頭所述,當一個執行緒修改了這個變數的值,volatile 保證了新值能立即同步到主記憶體,以及每次使用前立即從主記憶體重新整理。但普通變數做不到這點,普通變數的值線上程間傳遞均需要通過主記憶體(詳見:Java記憶體模型)來完成。

  2.禁止指令重排序優化。有volatile修飾的變數,賦值後多執行了一個“load addl $0x0, (%esp)”操作,這個操作相當於一個記憶體屏障(指令重排序時不能把後面的指令重排序到記憶體屏障之前的位置),只有一個CPU訪問記憶體時,並不需要記憶體屏障;(什麼是指令重排序:是指CPU採用了允許將多條指令不按程式規定的順序分開發送給各相應電路單元處理)。

volatile 效能:

  volatile 的讀效能消耗與普通變數幾乎相同,但是寫操作稍慢,因為它需要在原生代碼中插入許多記憶體屏障指令來保證處理器不發生亂序執行。