1. volatile是什么?
在談及線程安全時,常會說到一個變量——volatile。在《Java並發編程實戰》一書中是這么定義volatile的——“Java語言提供了一種稍弱的同步機制,即volatile變量,用來確保將變量的更新操作通知到其他線程”。這句話說明了兩點:①volatile變量是一種同步機制;②volatile能夠確保可見性。這兩點和我們探討“volatile變量是否能夠保證線程安全性”息息相關。
2. volatile變量能確保線程安全性嗎?為什么?
什么是同步機制?在並發程序設計中,各進程對公共變量的訪問必須加以制約,這種制約稱為同步。也就是說,同步機制即為對共享資源的一種制約。那么問題來了:volatile這種“稍弱的同步機制”是怎么制約各個進程對共享資源的訪問的呢?答案就在“volatile能夠確保可見性”中。
2.1 可見性
volatile能夠保證字段的可見性:volatile變量,用來確保將變量的更新操作通知到其他線程。volatile變量不會被緩存在寄存器或者對其他處理器不可見的地方,因此在讀取volatile類型的變量時總會返回最新寫入的值。
可見性和“線程如何對變量進行操作(取值、賦值等)”有關系:
我們要先明確一個定律:線程對變量的所有操作(取值、賦值等)都必須在工作內存(各線程獨立擁有)中進行,而不能直接讀寫內存中的變量,各工作內存間也不能相互訪問。對於volatile變量來說,由於它特殊的操作順序性規定,看起來如同操作主內存一般,但實際上 volatile變量也是遵循這一定律的。
關於主存與工作內存之間具體的交互協議(即一個變量如何從主存拷貝到工作內存、如何從工作內存同步到主存等實現細節),Java內存模型中定義了以下八種操作來完成:
lock:(鎖定),unlock(解鎖),read(讀取),load(載入),use(試用), assign(賦值),store(存儲),write(寫入)。
volatile 對這八種操作有着兩個特殊的限定,正因為有這些限定才讓volatile修飾的變量有可見性以及可以禁止指令重排序 :
① use動作之前必須要有read和load動作, 這三個動作必須是連續出現的。【表示:每次工作內存要使用volatile變量之前必須去主存中拿取最新的volatile變量】
② assign動作之后必須跟着store和write動作,這三個動作必須是連續出現的。【表示: 每次工作內存改變了volatile變量的值,就必須把該值寫回到主存中】
有以上兩條規則就能保證每個線程每次去拿volatile變量的時候,那個變量肯定是最新的, 其實也就相當於好多個線程用的是同一個內存,無工作內存和主存之分。而操作沒有用volatile修飾的變量則不能保證每次都能獲取到最新的變量值。
2.2 所以volatile究竟能否保證線程安全性?
答:在某些特定的情況下能。那到底什么是什么能,什么時候又不能了呢?我們繼續往下看。
(1)volatile能保證線程安全的情況
要使 volatile 變量提供理想的線程安全性,必須同時滿足兩個條件:①對變量的寫操作不依賴於當前值。②該變量沒有包含在具有其他變量的不變式中。
實際上,這兩個條件表明,可以被寫入 volatile 變量的這些有效值獨立於任何程序的狀態,包括變量的當前狀態。大多數編程情形都會與這兩個條件的其中之一沖突(如:“若沒有則添加”、“若相等則移出”的復合操作等復合操作都是與①或②相沖突的),使得 volatile 變量不能像 synchronized 那樣普遍適用於實現線程安全。
(2)volatile不能保證線程安全的情況
除了(1)中提到的能夠使volatile發揮保證線程安全性的情況,其他情況下volatile並不能保證線程安全問題,因為volatile並不能保證變量操作的原子性。
我們先以 i++( i++ 是非原子操作)為例:
private volatile int i = 0,兩個線程同時執行 i++,
此時是兩個線程同時從主內存中拿到 i 的最新值 0 ,並且同時對 i 進行 +1 操作並將和賦值回 i,最后同時將 +1 后的 i 值寫回主內存中,最終 i == 1,很明顯結果是錯的。
下面我們再通過更詳細的代碼來驗證“即使變量用了volatile來修飾,才進行非原子操作時依舊會出現線程安全問題”:
class Window implements Runnable { private volatile int ticket = 100; public void run() { for (;;) { //通過下面的①②兩個步驟我們可以發現:對一個共享資源可以多個線程同時進行修改,自然就會有線程安全問題。 if (ticket > 0) { try { Thread.sleep(100);//①多個線程同時判斷到“ticket>0”,然后掛起了 } catch (InterruptedException e) { e.printStackTrace(); } //②多個線程同時醒來,同時進行“ticket--”操作: System.out.println(Thread.currentThread().getName() + ":" + ticket--); } else { break; } } } }
public class A03UseVolatileIsNotThreadSafe { public static void main(String[] args) { Window w = new Window(); Thread t1 = new Thread(w); Thread t2 = new Thread(w); Thread t3 = new Thread(w); t1.setName("窗口1"); t2.setName("窗口2"); t3.setName("窗口3"); t1.start(); t2.start(); t3.start(); } }
測試結果:(1)出現了大量的重復數字; (2)最后還輸出了 “-1”;==》說明變量即使用volatile修飾了但依舊出現了線程安全問題。
代碼解析:
出現問題(1)的原因:線程存在“先檢查后執行”的競態條件。可能有兩個線程同時擁有CPU的執行權(機器是雙核的),它們判斷到做“if (ticket > 0)”,並同時做“ticket--”操作。
出現問題(2)的原因:
①當ticket==1時,兩個或多個線程同時通過了“if (ticket > 0)”的判斷,並進入了判斷框中去執行代碼;
②然后它們執行到“Thread.sleep(100);”就睡了;
③睡醒后總有一個線程會先搶到cup的執行權,然后執行“ticket--”操作,並將最新的ticket數值推送告知到每個線程;
④此時那些在判斷框中的其他的線程並不會再次做“if (ticket > 0)”的判斷,而是直接拿最新的ticket並做“ticket--”操作。
就算線程在“ticket--”之前每次都做“if (ticket > 0)”的判斷,也依舊會有線程安全問題,因為又可能出現①那種同時通過判斷的狀態。
// volatile的典型用法:檢查某個狀態標記,以判斷是否退出循環。
volatile boolean asleep;
……
while( !asleep){ countSomeSheep(); }
3. volatile的典型用法:檢查某個狀態標記,以判斷是否退出循環。
volatile boolean asleep; …… while( !asleep){ countSomeSheep(); }
4. 總結:因為volatile不能保證變量操作的原子性,所以試圖通過volatile來保證線程安全性是不靠譜的。