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