變量不可見的兩個原因
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....