并发编程—Volatile关键字
鎖提供了兩種主要特性:互斥(mutual exclusion) 和可見性(visibility)。互斥即一次只允許一個線程持有某個特定的鎖,因此可以保證一次就只有一個線程在訪問共享數據。可見性要復雜一些,它必須確保釋放鎖之前對共享數據做出的更改對于隨后獲得該鎖的另一個線程是可見的。
volatile 變量可以被看作是一種 “輕量級的 synchronized”,與 synchronized 塊相比,volatile 變量所需的編碼較少,并且運行時開銷也較少,但是它所能實現的功能也僅是 synchronized 的一部分。
volatile變量
一個共享變量被volatile修飾之后,則具有了兩層語義:
保證內存可見性
前面講過Java內存模型,可以知道:對一個共享變量進行操作時,各個線程會將共享變量從主內存中拷貝到工作內存,然后CPU會基于工作內存中的數據進行處理。線程在工作內存進行操作完成之后何時會將結果寫回主內存中?這個時機對普通變量是沒有規定的。所以才導致了內存可見性問題。
volatile是如何解決可見性問題的?
如果代碼中的共享變量被volatile修飾,在生成匯編代碼時會在volatile修飾的共享變量進行寫操作的時候會多出Lock前綴的指令。在多核處理器的情況下,這個Lock指令主要有3個功能:
所以,被volatile修飾的變量能夠保證每個線程能夠獲取該變量的最新值,從而避免出現數據臟讀的現象。
禁止指令重排序
對于volatile的共享變量,編譯器在生成字節碼時,會在指令序列中插入內存屏障(Lock指令)來禁止特定類型的重排序。這是在happens-before的原則下做進一步的約束。
對于編譯器來說,發現一個最優布置來最小化插入屏障的總數幾乎是不可能的,為此,JMM采取了保守策略:
- 在每個volatile寫操作的前面插入一個StoreStore屏障;
- 在每個volatile寫操作的后面插入一個StoreLoad屏障;
- 在每個volatile讀操作的后面插入一個LoadLoad屏障;
- 在每個volatile讀操作的后面插入一個LoadStore屏障。
需要注意的是:volatile寫是在前面和后面分別插入內存屏障,而volatile讀操作是在后面插入兩個內存屏障。
- StoreStore屏障:禁止上面的普通寫和下面的volatile寫重排序;
- StoreLoad屏障:防止上面的volatile寫與下面可能有的volatile讀/寫重排序
- LoadLoad屏障:禁止下面所有的普通讀操作和上面的volatile讀重排序
- LoadStore屏障:禁止下面所有的普通寫操作和上面的volatile讀重排序
如下兩張圖來自《Java并發編程的藝術》一書:
- volatile變量的寫操作
- volatile變量的讀操作
根據上面的說明也能得出:雖然volatile關鍵字能禁止指令重排序,但是volatile也只能在一定程度上保證有序性。在volatile之前和之后的指令集不會亂序越過volatile變量執行,但volatile之前和之后的指令集在沒有關聯性的前提下,仍然會執行指令重排。
使用 volatile 變量的條件
volatile并不能代替synchronized,要使 volatile 變量提供理想的線程安全,必須同時滿足下面兩個條件:
對變量的寫操作不依賴于當前值。
例如i++的操作就無法通過volatile保證結果準確性的,因為i++包含了讀取-修改-寫入三個步驟,并不是一個原子操作,所以 volatile 變量不能用作線程的安全計數器。
例如下面的這段代碼,可以說明volatile變量的操作不具有原子性:
運行計數器的結果很大可能性是<1000的。對于計數器的這種功能,一般是需要使用JUC中atomic包下的類,利用CAS的機制去做。
該變量沒有包含在具有其他變量的不變式中。
這句話有點拗口,看代碼比較直觀。
上述代碼中,上下界初始化分別為0和10,假設線程A和B在某一時刻同時執行了setLower(8)和setUpper(5),且都通過了不變式的檢查,設置了一個無效范圍(8, 5),所以在這種場景下,需要使setLower()和setUpper()操作原子化 —— 而將字段定義為 volatile 類型是無法實現這一目的的。
使用 volatile 舉例
雖然使用 volatile 變量要比使用相應的鎖簡單得多,而且性能也更好,但是一般不會太多的使用它,主要是它比使用鎖更加容易出錯。
想要安全地使用volatile,必須牢記一條原則:只有在狀態真正獨立于程序內其他內容時才能使用 volatile
修飾狀態標志量
volatile boolean shutdownRequested;...public void shutdown() { shutdownRequested = true; }public void doWork() { while (!shutdownRequested) { // do stuff} }在這個示例使用 synchronized 塊編寫循環要比使用 volatile 狀態標志編寫麻煩很多。由于 volatile 簡化了編碼,并且狀態標志并不依賴于程序內任何其他狀態,因此此處非常適合使用 volatile。
double-check 單例模式
public class Singleton {private volatile static Singleton instance;public static Singleton getInstance() {if (instance == null) { //1syschronized(Singleton.class) { //2if (instance == null) { //3instance = new Singleton(); //4}}}return instance;} }為什么要用volatile修飾才是最安全的呢?可能有人會覺得是這樣:線程1執行完第4步,釋放鎖。線程2獲得鎖后執行到第4步,由于可見性的原因,發現instance還是null,從而初始化了兩次。
但是不會存在這種情況,因為synchronized能保證線程1在釋放鎖之前會講對變量的修改刷新到主存當中,線程2拿到的值是最新的。
實際存在的問題是無序性。
第4步這個new操作是無序的,它可能會被編譯成:
a.先分配內存,讓instance指向這塊內存
b.在內存中創建對象
synchronized雖然是互斥的,但不代表一次就把整個過程執行完,它在中間是可能釋放時間片的,時間片不是鎖。
也就是說可能在a執行完后,時間片被釋放,線程2執行到1,此時它讀到的instance是不是null呢?基于可見性,可能是null,也可能不是null。 有意思的是,在這個例子中,如果讀到的是null,反而沒問題了,接下來會等待鎖,然后再次判斷時不為null,最后返回單例。
如果讀到的不是null,按代碼邏輯直接return instance,但這個instance還沒執行構造參數,所以使用的時候就會出現問題。
總結
以上是生活随笔為你收集整理的并发编程—Volatile关键字的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: linux下工具exfs用法
- 下一篇: 活用变量字符串${var%%.*}