
Volatile有序性
在并發(fā)編程中談及到的無非是可見性、有序性及原子性。而這里的Volatile只能夠保證前兩個性質(zhì),對于原子性還是不能保證的,只能通過鎖的形式幫助他去解決原子性操作。
package com.montos.detail;
public class Singleton {
public static volatile Singleton instance = null;
private Singleton() {
}
public static Singleton getInstance() {
if (instance == null) {
synchronized (instance) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
上面的代碼是利用了單例模式里面的一個雙重校驗的寫法,里面的實例變量中就是加上了volatile關(guān)鍵字,可能大家對于加不加這個關(guān)鍵字沒啥感覺,因為去除這個關(guān)鍵字就可以保證多線程的情況下,外部能夠拿到唯一的對象,還需要加上這個關(guān)鍵字干什么?。
雙重校驗的寫法:第一次判斷是否為null是為了拒絕掉當對象不為空的時候剩余的線程。里面加鎖是為了當對象為null的時候,此時同時進來兩個線程(A和B兩個線程),我們要保證只有一個線程才可以初始化對象,所以在這里面加上了鎖,這樣A拿到了鎖進去初始化對象,然后進行返回,B再進去此時發(fā)現(xiàn)不為null,那么就不執(zhí)行初始化的過程。這樣就能保證上面的單例模式的正常運行,同時為系統(tǒng)也是節(jié)約了許多開銷(避免每個線程進來加鎖--懶漢式寫法等。。)
在理解上面的為什么不安全的情況下,我們首先要理解對象實例化的步驟:
- 分配內(nèi)存空間。
- 初始化對象。
- 將內(nèi)存空間的地址賦值給對應的引用。
上面是正常情況下,對象實例化的步驟,但是由于操作系統(tǒng)方面的原因。上面的第二步可能與第三步進行對換,如果發(fā)生這種情況,那么此時拿到的對象也只是一個引用,對于后面的業(yè)務操作可能存在錯誤的發(fā)生。
| 序號 | 指令 | 說明 |
|---|---|---|
| 1 | IF | 取值 |
| 2 | ID | 譯碼和取寄存器操作數(shù) |
| 3 | EX | 執(zhí)行或者有效地址計算 |
| 4 | MEM | 存儲器訪問 |
| 5 | WB | 寫回 |
未進行指令重排的Demo:
a = b + c; d = e -f ;

從上圖可以看到有幾個打x的地方,如果按照順序執(zhí)行的話,CPU是需要一個時鐘周期來等待的,首先看第一個紅色框的,第一個需要空出一個時鐘周期是因為當前變量C還沒有寫入,此時是不可以進行兩個值計算的,我們需要等待變量C的寫入才可以進行執(zhí)行兩個數(shù)的求和,第二個空的時鐘周期是因為當前一個時鐘周期內(nèi),一個物理邏輯單位只能被一個指令執(zhí)行,如果不空出一個時鐘周期,那么就會與上面的EX起到?jīng)_突,第三個空檔也是一樣的道理。第二個紅色框也是如此。
這上面就是如果計算機不進行指令重排的話,一個簡單的計算,我們就可能浪費了5個時鐘周期,即一條指令的從頭到尾執(zhí)行,所以計算機為了高效,就會對原來的指令進行重排,讓CPU的資源能夠得到很好的使用。
我們就將變量e的指令執(zhí)行放在變量c之后,變量f的指令執(zhí)行放在計算第一個表達式指令之后:

結(jié)果我們看到:

這個時候我們發(fā)現(xiàn)并沒有浪費一個時鐘周期,程序也達到了想要的計算效果,這就是計算機對于指令重排的一個優(yōu)點,使得流水線更加的順暢。
上面就說明了指令重排有時候?qū)τ诔绦驁?zhí)行是好的,但是有些情況下我們并不想發(fā)生這種情況,就是對象實例化的時候,我們就希望它能夠按照順序執(zhí)行的方式執(zhí)行下去。這個時候volatile就幫助了我們,它能夠有效的防止指令重排。
Volatile有序性原理
volatile之所以能夠阻止指令重排,是因為底層JVM里面利用了內(nèi)存屏障來實現(xiàn)的,內(nèi)存屏障主要有三點功能:
- 它確保指令重排序時不會把其后面的指令排到內(nèi)存屏障之前的位置,也不會把前面的指令排到內(nèi)存屏障的后面;即在執(zhí)行到內(nèi)存屏障這句指令時,在它前面的操作已經(jīng)全部完成;
- 它會強制將對緩存的修改操作立即寫入主存;
- 如果是寫操作,它會導致其他CPU中對應的緩存行無效。
這里主要有四種類型的屏障操作:
(1)LoadLoad 屏障
執(zhí)行順序:Load1—>Loadload—>Load2
確保Load2及后續(xù)Load指令加載數(shù)據(jù)之前能訪問到Load1加載的數(shù)據(jù)。
(2)StoreStore 屏障
執(zhí)行順序:Store1—>StoreStore—>Store2
確保Store2以及后續(xù)Store指令執(zhí)行前,Store1操作的數(shù)據(jù)對其它處理器可見。
(3)LoadStore 屏障
執(zhí)行順序: Load1—>LoadStore—>Store2
確保Store2和后續(xù)Store指令執(zhí)行前,可以訪問到Load1加載的數(shù)據(jù)。
(4)StoreLoad 屏障
執(zhí)行順序: Store1—> StoreLoad—>Load2
確保Load2和后續(xù)的Load指令讀取之前,Store1的數(shù)據(jù)對其他處理器是可見的。
通過上面內(nèi)存屏障的限制,我們使用volatile就可以保證指令不會被操作系統(tǒng)進行重排。
Volatile可見性
線程本身并不直接與主內(nèi)存進行數(shù)據(jù)的交互,而是通過線程的工作內(nèi)存來完成相應的操作。這也是導致線程間數(shù)據(jù)不可見的本質(zhì)原因。因此要實現(xiàn)volatile變量的可見性,直接從這方面入手即可。對volatile變量的寫操作與普通變量的主要區(qū)別有兩點:
修改volatile變量時會強制將修改后的值刷新的主內(nèi)存中。
修改volatile變量后會導致其他線程工作內(nèi)存中對應的變量值失效。因此,再讀取該變量值的時候就需要重新從讀取主內(nèi)存中的值。
通過這兩點就可以很好的解決可見性問題。
寫在最后
