轉(zhuǎn)載請注明原創(chuàng)出處,謝謝旁趟!
簡書占小狼
http://www.reibang.com/users/90ab66c248e6/latest_articles
前言
看著上一篇的更新時間,發(fā)現(xiàn)已經(jīng)挺長時間沒有提筆了桦山,只能以忙為自己開脫了本慕,如果太閑都不好意思說自己是程序猿了,正好今天有人問了我一個問題:
當一個共享變量被volatile修飾時唱矛,它會保證修改的值立即被更新到主存“, 這里的”保證“ 是如何做到的井辜?和 JIT的具體編譯后的CPU指令相關吧绎谦?
最一開始碰到volatile,我的內(nèi)心是拒絕的粥脚,因為當時做的項目中沒有用到窃肠,也不清楚可以在什么場景下使用,所以希望這篇文章可以幫助大家理解volatile關鍵字刷允。
volatile特性
內(nèi)存可見性:通俗來說就是冤留,線程A對一個volatile變量的修改碧囊,對于其它線程來說是可見的,即線程每次獲取volatile變量的值都是最新的纤怒。
volatile的使用場景
通過關鍵字sychronize可以防止多個線程進入同一段代碼糯而,在某些特定場景中,volatile相當于一個輕量級的sychronize泊窘,因為不會引起線程的上下文切換熄驼,但是使用volatile必須滿足兩個條件:
1、對變量的寫操作不依賴當前值州既,如多線程下執(zhí)行a++谜洽,是無法通過volatile保證結(jié)果準確性的;
2吴叶、該變量沒有包含在具有其它變量的不變式中,這句話有點拗口序臂,看代碼比較直觀蚌卤。
public class NumberRange {
private volatile int lower = 0;
private volatile int upper = 10;
public int getLower() { return lower; }
public int getUpper() { return upper; }
public void setLower(int value) {
if (value > upper)
throw new IllegalArgumentException(...);
lower = value;
}
public void setUpper(int value) {
if (value < lower)
throw new IllegalArgumentException(...);
upper = value;
}
}
上述代碼中,上下界初始化分別為0和10奥秆,假設線程A和B在某一時刻同時執(zhí)行了setLower(8)和setUpper(5)逊彭,且都通過了不變式的檢查,設置了一個無效范圍(8, 5)构订,所以在這種場景下侮叮,需要通過sychronize保證方法setLower和setUpper在每一時刻只有一個線程能夠執(zhí)行。
下面是我們在項目中經(jīng)常會用到volatile關鍵字的兩個場景:
1悼瘾、狀態(tài)標記量
在高并發(fā)的場景中囊榜,通過一個boolean類型的變量isopen,控制代碼是否走促銷邏輯亥宿,該如何實現(xiàn)卸勺?
public class ServerHandler {
private volatile isopen;
public void run() {
if (isopen) {
//促銷邏輯
} else {
//正常邏輯
}
}
public void setIsopen(boolean isopen) {
this.isopen = isopen
}
}
場景細節(jié)無需過分糾結(jié),這里只是舉個例子說明volatile的使用方法烫扼,用戶的請求線程執(zhí)行run方法曙求,如果需要開啟促銷活動,可以通過后臺設置映企,具體實現(xiàn)可以發(fā)送一個請求悟狱,調(diào)用setIsopen方法并設置isopen為true,由于isopen是volatile修飾的堰氓,所以一經(jīng)修改挤渐,其他線程都可以拿到isopen的最新值,用戶請求就可以執(zhí)行促銷邏輯了豆赏。
2挣菲、double check
單例模式的一種實現(xiàn)方式富稻,但很多人會忽略volatile關鍵字,因為沒有該關鍵字白胀,程序也可以很好的運行椭赋,只不過代碼的穩(wěn)定性總不是100%,說不定在未來的某個時刻或杠,隱藏的bug就出來了哪怔。
class Singleton {
private volatile static Singleton instance;
public static Singleton getInstance() {
if (instance == null) {
syschronized(Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
不過在眾多單例模式的實現(xiàn)中,我比較推薦懶加載的優(yōu)雅寫法Initialization on Demand Holder(IODH)向抢。
public class Singleton {
static class SingletonHolder {
static Singleton instance = new Singleton();
}
public static Singleton getInstance(){
return SingletonHolder.instance;
}
}
當然认境,如果不需要懶加載的話,直接初始化的效果更好挟鸠。
如何保證內(nèi)存可見性叉信?
在java虛擬機的內(nèi)存模型中,有主內(nèi)存和工作內(nèi)存的概念艘希,每個線程對應一個工作內(nèi)存硼身,并共享主內(nèi)存的數(shù)據(jù),下面看看操作普通變量和volatile變量有什么不同:
1覆享、對于普通變量:讀操作會優(yōu)先讀取工作內(nèi)存的數(shù)據(jù)佳遂,如果工作內(nèi)存中不存在,則從主內(nèi)存中拷貝一份數(shù)據(jù)到工作內(nèi)存中撒顿;寫操作只會修改工作內(nèi)存的副本數(shù)據(jù)丑罪,這種情況下,其它線程就無法讀取變量的最新值凤壁。
2吩屹、對于volatile變量,讀操作時JMM會把工作內(nèi)存中對應的值設為無效客扎,要求線程從主內(nèi)存中讀取數(shù)據(jù)祟峦;寫操作時JMM會把工作內(nèi)存中對應的數(shù)據(jù)刷新到主內(nèi)存中,這種情況下徙鱼,其它線程就可以讀取變量的最新值宅楞。
volatile變量的內(nèi)存可見性是基于內(nèi)存屏障(Memory Barrier)實現(xiàn)的,什么是內(nèi)存屏障袱吆?內(nèi)存屏障厌衙,又稱內(nèi)存柵欄,是一個CPU指令绞绒。在程序運行時婶希,為了提高執(zhí)行性能,編譯器和處理器會對指令進行重排序蓬衡,JMM為了保證在不同的編譯器和CPU上有相同的結(jié)果喻杈,通過插入特定類型的內(nèi)存屏障來禁止特定類型的編譯器重排序和處理器重排序彤枢,插入一條內(nèi)存屏障會告訴編譯器和CPU:不管什么指令都不能和這條Memory Barrier指令重排序。
這段文字顯得有點蒼白無力筒饰,不如來段簡明的代碼:
class Singleton {
private volatile static Singleton instance;
private int a;
private int b;
private int b;
public static Singleton getInstance() {
if (instance == null) {
syschronized(Singleton.class) {
if (instance == null) {
a = 1; // 1
b = 2; // 2
instance = new Singleton(); // 3
c = a + b; // 4
}
}
}
return instance;
}
}
1缴啡、如果變量instance沒有volatile修飾,語句1瓷们、2业栅、3可以隨意的進行重排序執(zhí)行,即指令執(zhí)行過程可能是3214或1324谬晕。
2碘裕、如果是volatile修飾的變量instance,會在語句3的前后各插入一個內(nèi)存屏障攒钳。
通過觀察volatile變量和普通變量所生成的匯編代碼可以發(fā)現(xiàn)帮孔,操作volatile變量會多出一個lock前綴指令:
Java代碼:
instance = new Singleton();
匯編代碼:
0x01a3de1d: movb $0x0,0x1104800(%esi);
0x01a3de24: **lock** addl $0x0,(%esp);
這個lock前綴指令相當于上述的內(nèi)存屏障,提供了以下保證:
1不撑、將當前CPU緩存行的數(shù)據(jù)寫回到主內(nèi)存你弦;
2、這個寫回內(nèi)存的操作會導致在其它CPU里緩存了該內(nèi)存地址的數(shù)據(jù)無效燎孟。
CPU為了提高處理性能,并不直接和內(nèi)存進行通信尸昧,而是將內(nèi)存的數(shù)據(jù)讀取到內(nèi)部緩存(L1揩页,L2)再進行操作,但操作完并不能確定何時寫回到內(nèi)存烹俗,如果對volatile變量進行寫操作爆侣,當CPU執(zhí)行到Lock前綴指令時,會將這個變量所在緩存行的數(shù)據(jù)寫回到內(nèi)存幢妄,不過還是存在一個問題兔仰,就算內(nèi)存的數(shù)據(jù)是最新的,其它CPU緩存的還是舊值蕉鸳,所以為了保證各個CPU的緩存一致性乎赴,每個CPU通過嗅探在總線上傳播的數(shù)據(jù)來檢查自己緩存的數(shù)據(jù)有效性,當發(fā)現(xiàn)自己緩存行對應的內(nèi)存地址的數(shù)據(jù)被修改潮尝,就會將該緩存行設置成無效狀態(tài)榕吼,當CPU讀取該變量時,發(fā)現(xiàn)所在的緩存行被設置為無效勉失,就會重新從內(nèi)存中讀取數(shù)據(jù)到緩存中羹蚣。
END。
我是占小狼乱凿。
在魔都艱苦奮斗顽素,白天是上班族咽弦,晚上是知識服務工作者。
如果讀完覺得有收獲的話胁出,記得關注和點贊哦型型。
非要打賞的話,我也是不會拒絕的划鸽。