volatile、Synchronized實現變量可見性的原理,volatile使用注意事項


變量不可見的兩個原因

Java每個線程工作都有一個工作空間,需要的變量都是從主存中加載進來的。Java內存模型如下(JMM):

 

線程訪問一個共享的變量時,都需要先從主存中加載一個副本到自己的工作內存中,經過自己修改后再更新到主存中去。在這個過程中可能出現這種情況:線程A在工作內存中修改了變量1的值,但是還沒有寫入主存,這檔口線程B將變量1加載到自己工作內存中。顯然,線程B拿到的不是變量1的最新值了。

變量可見性就是: 這個變量被任何一個線程修改了,其他線程都能“看見”,也就是能取到變量最新的值。

 

重排序是指為了適合cpu指令執行機制,編譯器、內存系統、處理器可能會對一些指令的執行順序進行重排。例如:

int a = 1;               //line1
int b = 2;                //line2
int s = a*b;        //line3

line1 和 line2 可能會被重排顛倒位置,但是line3不會重排,因為Java單線程下會遵守 as-if-serial語義,簡單的講就是重排指令不會出現錯誤的結果。在多線程下,指令重排則可能造成一些問題,例如:

class Example {
int a = 0;
boolean flag = false;
 
public void writer() {
    a = 1;                   
    flag = true;           
}
 
public void reader() {
    if (flag) {                
        int i =  a +1;      
    }
}
}

線程A首先執行writer()方法,線程B線程接着執行reader()方法。線程B在int i=a+1 時不一定能看到a已經被賦值為1,因為在writer()中,兩句話順序可能打亂:

線程A執行順序:  flag=true;(a=1;還沒執行完或還沒寫到主存)

線程B執行:flag=true  (而此時a=0)    產生了一些與我們預期之外的情況。

導致變量不可見的原因(1)更新不及時,(2)多線程交替執行時的指令重排序

 

volatile實現可見性的原理

JVM線程工作時的原子性指令有:

    read: 從主存讀取一個變量的值的副本到線程的工作內存。

    load:把read來的值賦給工作空間的變量中,然后就可以使用了。

    use:要使用一個變量,先發出這個指令。

    assign:賦值,給變量一個新值。

 

    store:將工作空間的變量值運送到主存中。

 

    write:將值寫到主存的那個變量中。

上述操作必定是順序執行的,但可不一定連續,中間可能插入其他指令。為了保證可見性:關鍵就是保證load、use的執行順序不被打亂(保證使用變量前一定剛剛進行了load操作,從主存拿最新值來),assign、wirte的執行順序不被打亂(保證賦值后馬上就是把值寫到主存)。

所以使用內存屏障, CPU指令,可以禁止指令執行亂序:插入一個內存屏障, 相當於告訴CPU和編譯器指令順序先於這個指令的必須先執行,后於這個命令的必須后執行。

解決第一個導致不可見的因素(更新不及時):內存屏障,對於volatile修飾的變量,讀操作時在讀指令use插入一條讀屏障指令重新從主存加載最新值進來,保證了load、use指令的執行順序不亂;寫操作時在寫指令assign插入一條寫屏障指令,將工作內存變量的最新值立刻寫入主存變量。

解決第二個因素(指令重排): 由於讀寫數據時會在之前/后插入一條內存屏障指令,因此volatile可以禁止指令重排序。

 

Synchronized實現可見性原理

解決第一個因素:在加鎖前會將工作內存的值全部重新加載一遍,保證最新;釋放鎖前將工作內存的值全部更新到主存;由於在帶鎖期間,沒有其他線程能訪問本線程正在使用的共享變量,這樣就保證了可見性。

解決第二個因素: 由於Synchronized修飾的代碼塊都是原子性執行的,即一旦開始做,就會一直執行完畢,期間有其他線程不可以訪問本線程所使用的共享變量,這樣,即便指令重排了也不會出現問題。

 

volatile不具有原子性可能導致的問

經過前面的總結,可以看出Synchronized包裹的代碼里的共享變量有可見性、指令不可排序性、原子性;volatile修飾的變量有可見性、指令不可排序性;volatile並不保證變量有原子性。如果用volatile修飾變量希望保證它的原子性就可能出現問題,例如:

volatile  int num = 0;

線程A:
      num++;

線程B:
      num++;

兩個線程操作都進行num++的操作,理論上完事后 num 的值為2,但是num++ 的操作本身不是原子性的(包括讀取 num原先的值、+1、把+1后的值寫入num),volatile也不能使對num的操作變為原子性。因此可能有num = 1的結果:

       線程A:讀取了num的值,為0,然后阻塞了。

       線程B:讀取num的值,還是為0,+1后立即寫入主存,num = 1(體現可見性)。

        線程A:恢復執行了,已經做完讀取操作,工作內存中num = 0,繼續執行。num+1,寫入內存,num = 1;

雖然這種情況發生的概率很小,但是並發量大時還是會出現不少這種情況的(可以用Synchronized、lock、AtomicInteger等解決)。

由於原子性的問題,volatile不適合修飾依賴自身的變量 :  num++、a = a*2...    也不適合修飾不變式的變量(即volatile變量應該是獨立的):a<b.... 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM