volatile
被稱爲輕量級的synchronized,運行時開銷比synchronized
更小,在多線程併發編程中發揮着同步共享變量、禁止處理器重排序的重要做用。建議在學習volatie
以前,先看一下Java內存模型《什麼是Java內存模型?》,由於volatile
和Java內存模型有着莫大的關係。html
在學習volatie
以前,須要補充下Java內存模型的相關(JMM)知識,咱們知道Java線程的全部操做都是在工做區進行的,那麼工做區和主存之間的變量是怎麼進行交互的呢,能夠用下面的圖來表示。java
read
操做傳過來的變量值儲存到工做區內存的變量副本中。store
操做傳過來的值賦值給主存變量。這8
個操做每一個操做都是原子性的,可是幾個操做連着一塊兒就不是原子性了!c++
上面介紹了Java模型的8
個操做,那麼這8
個操做和volatile
又有着什麼關係呢。面試
什麼是可見性,用一個例子來解釋,先看一段代碼,加入線程1
先執行,線程2
再執行編程
//線程1
boolean stop = false;
while (!stop) {
do();
}
//線程2
stop = true;
複製代碼
線程1
執行後會進入到一個死循環中,當線程2
執行後,線程1
的死循環就必定會立刻結束嗎?答案是不必定,由於線程2
執行完stop = true
後,並不會立刻將變量stop
的值true
寫回主存中,也就是上圖中的assign
執行完成以後,store
和write
並不會隨着執行,線程1
沒有當即將修改後的變量的值更新到主存中,即便線程2
及時將變量stop
的值寫回主存中了,線程1
也沒有了解到變量stop
的值已被修改而去主存中從新獲取,也就是線程1
的load
、read
操做並不會立刻執行形成線程1
的工做區內存中的變量副本不是最新的。這兩個緣由形成了線程1
的死循環也就不會立刻結束。
那麼如何避免上訴的問題呢?咱們可使用volatile
關鍵字修飾變量stop
,以下bash
//線程1
volatile boolean stop = false;
while (!stop) {
do();
}
//線程2
stop = true;
複製代碼
這樣線程1
每次讀取變量stop
的時候都會先去主存中獲取變量stop
最新的值,線程2
每次修改變量stop
的值以後都會立刻將變量的值寫回主存中,這樣也就不會出現上述的問題了。多線程
那麼關鍵字volatie
是如何作到的呢?volatie
規定了上述8
個操做的規則併發
load
時,線程才能對變量執行use
操做;只有線程的後一個操做是use
時,線程才能對變量執行load
操做。即規定了use
、load
、read
三個操做之間的約束關係,規定這三個操做必須連續的出現,保證了線程每次讀取變量的值前都必須去主存獲取最新的值。assign
時,線程才能對變量執行store
操做;只有線程的後一個操做是store
時,線程才能對變量執行assign
操做,即規定了assign
、store
、write
三個操做之間的約束關係,規定了這三個操做必須連續的出現,保證線程每次修改變量後都必須將變量的值寫回主存。volatile
的這兩個規則,也正是保證了共享變量的可見性。post
有序性即程序執行的順序按照代碼的前後順序執行,Java內存模型(JMM)容許編譯器和處理器對指令進行重排序,可是規定了as-if-serial
語義,即保證單線程狀況下無論怎麼重排序,程序的結果不能改變,如學習
double pi = 3.14; //A
double r = 1; //B
double s = pi * r * r; //C
複製代碼
上面的代碼可能按照A->B->C
順序執行,也有可能按照B->A->C
順序執行,這兩種順序都不會影響程序的結果。可是不會以C->A(B)->B(A)
的順序去執行,由於C
語句是依賴於A
和B
的,若是按照這樣的順序去執行就不能保證結果不變了(違背了as-if-serial
)。
上面介紹的是單線程的執行,無論指令怎麼重排序都不會影響結果,可是在多線程下就會出現問題了。
下面看個例子
double pi = 3.14;
double r = 0;
double s = 0;
boolean start = false;
//線程1
r = 10; //A
start = true; //B
//線程2
if (start) { //C
s = pi * r * r; //D
}
複製代碼
線程1
和線程2
同時執行,線程1
的A
和B
的執行順序多是A->B
或者B->A
(由於A和B之間沒有依賴關係,能夠指令重排序)。若是線程1
按照A->B
的順序執行,那麼線程2
執行後的結果s就是咱們想要的正確結果,若是線程1
按照B->A
的順序執行,那麼線程2
執行後的結果s可能就不是咱們想要的結果了,由於線程1
將變量stop
的值修改成true
後,線程2
立刻獲取到stop
爲true
而後執行C
語句,而後執行D
語句即s = 3.14 * 0 * 0
,而後線程1
再執行B
語句,那麼結果就是有問題了。
那麼爲了解決這個問題,咱們能夠在變量true
加上關鍵字volatile
double pi = 3.14;
double r = 0;
double s = 0;
volatile boolean start = false;
//線程1
r = 10; //A
start = true; //B
//線程2
if (start) { //C
s = pi * r * r; //D
}
複製代碼
這樣線程1
的執行順序就只能是A->B
了,由於關鍵字發揮了禁止處理器指令重排序的做用,因此線程2
的執行結果就不會有問題了。
那麼volatile
是怎麼實現禁止處理器重排序的呢?
編譯器會在編譯生成字節碼的時候,在加有volatile
關鍵字的變量的指令進行插入內存屏障來禁止特定類型的處理器重排序
咱們先看內存屏障有哪些及發揮的做用
StoreStore
屏障:禁止屏障上面變量的寫和下面全部進行寫的變量進行處理器重排序。StoreLoad
屏障:禁止屏障上面變量的寫和下面全部進行讀的變量進行處理器重排序。LoadLoad
屏障:禁止屏障上面變量的讀和下面全部進行讀的變量進行處理器重排序。LoadStore
屏障:禁止屏障上面變量的讀和下面全部進行寫的變量進行處理器重排序。再看volatile
是怎麼插入屏障的
volatile
變量的寫前面插入一個StoreStore
屏障。volatile
變量的寫後面插入一個StoreLoad
屏障。volatile
變量的讀後面插入一個LoadLoad
屏障。volatile
變量的讀後面插入一個LoadStore
屏障。注意:寫操做是在
volatile
先後插入一個內存屏障,而讀操做是在後面插入兩個內存屏障。
volatile
變量經過插入內存屏障禁止了處理器重排序,從而解決了多線程環境下處理器重排序的問題。
上面分別介紹了volatile
的可見性和有序性,那麼volatile
有原子性嗎?咱們先看一段代碼
public class Test {
public volatile int inc = 0;
public void increase() {
inc++;
}
public static void main(String[] args) {
final Test test = new Test();
for(int i=0;i<10;i++){
new Thread(){
public void run() {
for(int j=0;j<1000;j++)
test.increase();
};
}.start();
}
while(Thread.activeCount()>1) //保證前面的線程都執行完
Thread.yield();
System.out.println(test.inc);
}
}
複製代碼
咱們開啓10
個線程對volatile
變量進行自增操做,每一個線程對volatile
變量執行1000
次自增操做,那結果變量inc
會是10000
嗎?答案是,變量inc
的值基本都是小於10000
。
可能你會有疑問,volatile
變量inc
不是保證了共享變量的可見性了嗎,每次線程讀取到的都是最新的值,是的沒錯,可是線程每次將值寫回主存的時候並不能保證主存中的值沒有被其餘的線程修過過。
若是所示:線程1
在主存中獲取了i
的最新值(i=1),線程2
也在主存中獲取了i
的最新值(i=1,注意這時候線程1
並未對變量i
進行修改,因此i
的值仍是1
)),而後線程2
將i自增後寫回主存,這時候主存中i=2
,到這裏尚未問題,而後線程1
又對i進行了自增寫回了主存,這時候主存中i=2
,也就是對i作了2次自增操做,結果i的結果只自增了1,問題就出來了這裏。
爲何會有這個問題呢,前面咱們提到了Java內存模型和主存之間交互的8
個操做都是原子性的,可是他們的操做連在一塊兒就不是原子性了,而volatile
關鍵字也只是保證了use
、load
、read
三個操做連在一塊兒時候的原子性,還有assign
、store
、write
這三個操做連在一塊兒時候的原子性,也就是volatile
關鍵字保證了變量讀操做的原子性和寫操做的原子性,而變量的自增過程須要對變量進行讀和寫兩個過程,而這兩個過程連在一塊兒就不是原子性操做了。
因此說volatile
變量對於變量的單獨寫操做/讀操做是保證了原子性的,而常說的原子性包括讀寫操做連在一塊兒,因此說對於volatile
不保證原子性的。那麼如何解決上面程序的問題呢?只能給increase
方法加鎖,讓在多線程狀況下只有一個線程能執行increase
方法,也就是保證了一個線程對變量的讀寫是原子性的。固然還有個更優的方案,就是利用讀寫都爲原子性的CAS
,利用CAS
對volatile
進行操做,既解決了volatile
不保證原子性的問題,同時消耗也沒加鎖的方式大
學完volatile
以後,是否是以爲volatile
和CAS
有種似曾相識的感受?那它們之間有什麼關係或者區別呢。
volatile
只能保證共享變量的讀和寫操做單個操做的原子性,而CAS
保證了共享變量的讀和寫兩個操做一塊兒的原子性(即CAS是原子性操做的)。volatile
的實現基於JMM
,而CAS
的實現基於硬件。Java併發編程:volatile關鍵字解析
JAVA併發六:完全理解volatile
Java內存模型與volatile
Java面試官最愛問的volatile關鍵字
原文地址:ddnd.cn/2019/03/19/…