1. 程式人生 > >Java執行緒安全之volatile關鍵字

Java執行緒安全之volatile關鍵字

一、前言

我們知道在多執行緒的場景下,執行緒安全是必須要著重考慮的。Java語言包含兩種內在的同步機制:同步塊(synchronize關鍵字)和 volatile 變數。但是其中 Volatile 變數雖然使用簡單,有時候開銷也比較低,但是同時它的同步性較差,而且其使用也更容易出錯。下面我們先使用一個例子來展示下volatile有可能出現執行緒不安全的情況:

public class ShareDataVolatile {
    //同時建立十個執行緒,每個執行緒自增100次
    //主程式等待3秒讓所有執行緒全部執行完畢後輸出最後的count值

    //使用volatile修飾計數變數count
public volatile static int count=0; public static void main(String[] args){ final ShareDataVolatile data = new ShareDataVolatile(); for(int i=0;i<10;i++){ new Thread( new Runnable(){ public void run(){ try
{ Thread.sleep(1); }catch(InterruptedException e){ e.printStackTrace(); } for(int j=0;j<100;j++){ data.addCount(); } System.out.print(count+" "
); } } ).start(); } try{ Thread.sleep(3000); }catch(InterruptedException e){ e.printStackTrace(); } System.out.println(); System.out.print("count="+count); } public void addCount(){ count++; } }

執行結果:
200 200 416 585 755 742 513 513 501 855
count=855
多次執行結果最後的count都不是預計的1000,這說明使用volatile變數並不能保證執行緒安全。

二、原因分析

鎖提供了兩種主要特性:互斥(mutual exclusion) 和可見性(visibility)。互斥即一次只允許一個執行緒持有某個特定的鎖,因此可使用該特性實現對共享資料的協調訪問協議,這樣,一次就只有一個執行緒能夠使用該共享資料。可見性要更加複雜一些,它必須確保釋放鎖之前對共享資料做出的更改對於隨後獲得該鎖的另一個執行緒是可見的 —— 如果沒有同步機制提供的這種可見性保證,執行緒看到的共享變數可能是修改前的值或不一致的值,這將引發許多嚴重問題。
Volatile 變數具有 synchronized 的可見性特性,但是不具備原子特性。這就是說執行緒能夠自動發現 volatile 變數的最新值。Volatile 變數可用於提供執行緒安全,但是隻能應用於非常有限的一組用例:多個變數之間或者某個變數的當前值與修改後值之間沒有約束。因此,單獨使用 volatile 還不足以實現計數器、互斥鎖或任何具有與多個變數相關的不變式(Invariants)的類(例如 “start <=end”)。
所以例子中雖然增量操作(count++)看上去類似一個單獨操作,實際上它是一個由讀取-修改-寫入操作序列組成的組合操作,必須以原子方式執行,而 volatile 不能對組合操作提供必須的原子特性。實現正確的操作需要使 count 的值在操作期間保持不變,而 volatile 變數無法實現這點。

三、JVM記憶體操作模型

在 java 垃圾回收整理一文中,描述了jvm執行時刻記憶體的分配。其中有一個記憶體區域是jvm虛擬機器棧,每一個執行緒執行時都有一個執行緒棧,執行緒棧儲存了執行緒執行時候變數值資訊。當執行緒訪問某一個物件時候值的時候,首先通過物件的引用找到對應在堆記憶體的變數的值,然後把堆記憶體變數的具體值load到執行緒本地記憶體中,建立一個變數副本,之後執行緒就不再和物件在堆記憶體變數值有任何關係,而是直接修改副本變數的值,在修改完之後的某一個時刻(執行緒退出之前),自動把執行緒變數副本的值回寫到物件在堆中變數。這樣在堆中的物件的值就產生變化了。下面一幅圖描述這寫互動:

這裡寫圖片描述

read and load 從主存複製變數到當前工作記憶體
use and assign 執行程式碼,改變共享變數值
store and write 用工作記憶體資料重新整理主存相關內容

其中use and assign 可以多次出現,但是這一些操作並不是原子性,也就是 在read load之後,如果主記憶體count變數發生修改之後,執行緒工作記憶體中的值由於已經載入,不會產生對應的變化,所以計算出來的結果會和預期不一樣對於volatile修飾的變數,jvm虛擬機器只是保證從主記憶體載入到執行緒工作記憶體的值是最新的,例如:
假如執行緒1,執行緒2 在進行ead,load 操作中,發現主記憶體中count的值都是5,那麼都會載入這個最新的值
線上程1堆count進行修改之後,會write到主記憶體中,主記憶體中的count變數就會變為6
執行緒2由於已經進行read,load操作,在進行運算之後,也會更新主記憶體count的變數值為6
導致兩個執行緒及時用volatile關鍵字修改之後,還是會存在併發的情況。

四、Volatile的優勢與使用條件

看了上面的,大家可能已經對volatile表示十分失望,不打算使用它了,然後volatile的存在肯定有它存在的意義:
1.簡易性:在某些情形下,使用 volatile 變數要比使用相應的鎖簡單得多。
2.效能:某些情況下,volatile 變數同步機制的效能要優於鎖。
對 JVM 內在的操作而言,我們難以抽象地比較 volatile 和 synchronized 的開銷。但是大部分情況下,在目前大多數的處理器架構上,volatile 讀操作開銷非常低 —— 幾乎和非 volatile 讀操作一樣。而 volatile 寫操作的開銷要比非 volatile 寫操作多很多,因為要保證可見性需要實現記憶體界定(Memory Fence),即便如此,volatile 的總開銷仍然要比鎖獲取低。
volatile 操作不會像鎖一樣造成阻塞,因此,在能夠安全使用 volatile 的情況下,volatile 可以提供一些優於鎖的可伸縮特性。如果讀操作的次數要遠遠超過寫操作,與鎖相比,volatile 變數通常能夠減少同步的效能開銷。

所以我們需要明確可以使用volatile的條件有兩點:
1.對變數的寫操作不依賴於當前值。
2.該變數沒有包含在具有其他變數的不變式中。

五、結束語

與鎖相比,Volatile 變數是一種非常簡單但同時又非常脆弱的同步機制,它在某些情況下將提供優於鎖的效能和伸縮性。如果嚴格遵循 volatile 的使用條件 —— 即變數真正獨立於其他變數和自己以前的值 —— 在某些情況下可以使用 volatile 代替 synchronized 來簡化程式碼。然而,使用 volatile 的程式碼往往比使用鎖的程式碼更加容易出錯。
另外如果不是很在意效能方面,並且希望實現簡潔明瞭的技術器功能,可以參考我部落格內的另一篇介紹AtomicInteger類的文章,該類可以實現原子性操作,從而保證執行緒安全:http://blog.csdn.net/roy_70/article/details/53160343