第一次理解:
剛學(xué)java時(shí)帅霜,對(duì)于volatile的記憶就是:
- volatile保證可見(jiàn)性
- volatile防止指令重排序
- volatile不保證原子性
沒(méi)過(guò)腦的背了一下,寫(xiě)代碼的時(shí)候也沒(méi)用到過(guò)惶看,以為不重要,然后就不了了之。
第二次理解
一段代碼引起好奇
class Singleton{
private volatile static Singleton instance = null;
private Singleton() {
}
public static Singleton getInstance() {
if(instance==null) {
synchronized (Singleton.class) {
if(instance==null)
instance = new Singleton();
}
}
return instance;
}
}
上圖為比較經(jīng)典的dcl(dubbo check lock)單例模式政基,雙重if判斷是為了防止多線程多次創(chuàng)建,但是instance屬性為什么還要加個(gè)volatile關(guān)鍵字呢闹啦,有什么作用么沮明?
其實(shí)它的作用主要體現(xiàn)在禁止指令重排序。
首先先理解下什么叫指令重排序窍奋?
指令重排序可以說(shuō)是jvm對(duì)程序執(zhí)行的一個(gè)優(yōu)化珊擂,他可以保證普通的變量在方法執(zhí)行的過(guò)程中所有依賴賦值結(jié)果的地方都能獲取到正確的結(jié)果,而不能保證變量賦值操作的順序與程序代碼中寫(xiě)的順序保持一致费变。如
x = 1;
y = 2;
這兩條賦值語(yǔ)句之間沒(méi)有依賴關(guān)系摧扇,所以在具體執(zhí)行時(shí)可能會(huì)先賦值y在賦值x,發(fā)生了指令重排挚歧。
而上述DCL代碼中雖然表面只有這instance = new Singleton();一條語(yǔ)句扛稽,但是這個(gè)賦值操作編譯成字節(jié)碼文件后是分為3個(gè)步驟來(lái)完成的:
- 為對(duì)象開(kāi)辟內(nèi)存空間并賦默認(rèn)值
- 調(diào)用構(gòu)造函數(shù)為對(duì)象賦初始值
- 將instance引用指向剛開(kāi)辟的內(nèi)存地址
而程序在執(zhí)行這三步時(shí),會(huì)有可能先執(zhí)行3再執(zhí)行2滑负,如果發(fā)生這種情況在张,線程一先將引用指向地址,還沒(méi)來(lái)得及執(zhí)行構(gòu)造方法矮慕,線程二進(jìn)來(lái)判斷instance帮匾!=null 直接拿這半初始化的對(duì)象去使用,就出現(xiàn)了問(wèn)題痴鳄。
所以此處需要用volatile關(guān)鍵字來(lái)修飾變量瘟斜,禁止指令重排序情況的發(fā)生。那么volatile是如何做到禁止重排序的呢?
《深入理解java虛擬機(jī)》中這樣寫(xiě)道:
我們對(duì)volatile修飾的變量進(jìn)行編譯后發(fā)現(xiàn)螺句,在賦值操作后多執(zhí)行了一個(gè)“l(fā)ock addl $0x0,(%esp)”,這個(gè)操作相當(dāng)于一個(gè)內(nèi)存屏障(Memory Barrier 或 Memory Fence虽惭,指重排序時(shí)不能把后面的指令重排序到內(nèi)存屏障之前的位置)
也有別的博主這樣寫(xiě)道:
JMM為volatile加內(nèi)存屏障有以下4種情況:
在每個(gè)volatile寫(xiě)操作的前面插入一個(gè)StoreStore屏障,防止寫(xiě)volatile與后面的寫(xiě)操作重排序蛇尚。
在每個(gè)volatile寫(xiě)操作的后面插入一個(gè)StoreLoad屏障芽唇,防止寫(xiě)volatile與后面的讀操作重排序。
在每個(gè)volatile讀操作的后面插入一個(gè)LoadLoad屏障取劫,防止讀volatile與后面的讀操作重排序匆笤。
在每個(gè)volatile讀操作的后面插入一個(gè)LoadStore屏障,防止讀volatile與后面的寫(xiě)操作重排序谱邪。
第三次理解
那么保證可見(jiàn)性又是指什么東東疚膊?
要想理解這可見(jiàn)性,需要先了解java內(nèi)存模型(jmm)虾标。學(xué)過(guò)計(jì)算機(jī)的同學(xué)都知道多核cpu中每個(gè)cpu都有自己的高速緩存寓盗,如L1,L2,L3,且每個(gè)cpu之間的緩存是隔離的璧函,即數(shù)據(jù)不可見(jiàn)傀蚌。而多個(gè)cpu又共享一個(gè)主內(nèi)存,數(shù)據(jù)一般會(huì)從磁盤(pán)讀取到主內(nèi)存當(dāng)中蘸吓,當(dāng)cpu需要處理數(shù)據(jù)時(shí)善炫,需要從主內(nèi)存讀取數(shù)據(jù)到自己的緩存當(dāng)中然后進(jìn)行運(yùn)算,運(yùn)算結(jié)束后將最新數(shù)據(jù)同步回內(nèi)存之中库继。當(dāng)然這種模型也伴隨這緩存一致性問(wèn)題的出現(xiàn)箩艺。
其實(shí)java內(nèi)存模型和cpu模型非常的類(lèi)似:
每個(gè)線程擁有自己的工作內(nèi)存,然后共享的變量會(huì)存放在主內(nèi)存(jvm的內(nèi)存)當(dāng)中宪萄,線程之間工作內(nèi)存互相隔離艺谆。如圖:
上圖來(lái)源于《深入理解java虛擬機(jī)363頁(yè)》
我們?cè)賮?lái)看個(gè)容易理解的圖:
再回到我們的保證可見(jiàn)性的探討:
如上圖所示,若線程A和B都操作主內(nèi)存的共享變量時(shí)拜英,AB會(huì)將共享變量先拷貝會(huì)自己的工作內(nèi)存静汤,在A率先完成修改完之后再同步刷回到主內(nèi)存當(dāng)中,此時(shí)線程B本地內(nèi)存的數(shù)據(jù)還是最先拷貝的舊數(shù)據(jù)居凶,沒(méi)有及時(shí)的獲取到已修改的最新數(shù)據(jù)虫给,最后會(huì)造成數(shù)據(jù)不一致問(wèn)題。
而volatile修飾變量時(shí)侠碧,它會(huì)保證修改的值會(huì)立即被更新到主存抹估,并通知其他線程當(dāng)前緩存的變量已失效,需要重新到主內(nèi)存中讀取弄兜。
底層也是通過(guò)內(nèi)存屏障來(lái)保證的药蜻。
針對(duì)這個(gè)特性瓷式,常見(jiàn)的使用的場(chǎng)景為狀態(tài)標(biāo)記量
public class VolatileTest1 {
volatile static boolean flag = false;
public static void main(String[] args) {
new Thread(() -> {
System.out.println("t1 start");
while (!flag){
System.out.println("doing something");
}
},"t1").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
flag = true;
}
}
和我們期望的一樣,1秒后谷暮,程序正常停止。
但是好奇的我開(kāi)始思考盛垦,那是不是只要不加volatile湿弦,程序就不會(huì)停止?立即更新的反義詞是什么腾夯?正常情況下颊埃,線程會(huì)不會(huì),什么時(shí)候會(huì)把修改的值寫(xiě)會(huì)主內(nèi)存蝶俱,別的線程又會(huì)什么時(shí)候會(huì)去重新讀劝嗬?
帶著好奇我把上訴代碼中的volatile去掉榨呆,運(yùn)行結(jié)果如圖:
沒(méi)錯(cuò) 程序居然正常停掉了罗标!
然后我又把while循環(huán)里的system輸出去掉后,再次運(yùn)行:
這次又沒(méi)有停止;摺闯割!
難道就是因?yàn)橐痪漭敵稣Z(yǔ)句的問(wèn)題么?我又嘗試換成i++試試:
這次也沒(méi)有停止8筒稹V胬!
很神奇丙笋,搞得我也很懵逼P怀骸!御板!
我不知道是不是因?yàn)榄h(huán)境的原因锥忿,我用的jdk11和8,idea2020.1.2怠肋,
個(gè)人初步猜測(cè):不加volatile缎谷,即正常情況下,本地線程更新值后灶似,會(huì)很快的寫(xiě)回主內(nèi)存列林,而其他線程什么時(shí)候重新從主內(nèi)存中讀取是不確定的。
上述while代碼里面執(zhí)行點(diǎn)稍微費(fèi)時(shí)的操作(如輸出酪惭,sleep 1s)希痴,都是可以停止的,如果循壞太快春感,它可能沒(méi)時(shí)間去重新讀取flag的值砌创。
(希望有大佬看到小弟的這篇文章虏缸,并指點(diǎn)一二。)
第四次理解
那不保證原子性又是什么鬼嫩实?
原子性:保證指令不會(huì)受到線程上下文切換的影響刽辙,即一個(gè)操作不會(huì)被cpu切換所打斷。
我們舉一個(gè)最常見(jiàn)的案列來(lái)說(shuō)明:
多個(gè)線程對(duì)同一個(gè)數(shù)字進(jìn)行++操作:
public class VolatileDemo {
public volatile int inc = 0;
public void increase() {
inc++;
}
public static void main(String[] args) {
final VolatileDemo test = new VolatileDemo();
System.out.println("start");
for (int i = 0; i < 20; i++) {
new Thread(() -> {
for (int j = 0; j < 10000; j++) {
test.increase();
}
}).start();
}
while (Thread.activeCount() > 2) {
Thread.yield();
}
System.out.println(test.inc);
}
}
我們啟動(dòng)了20個(gè)線程對(duì)inc進(jìn)行++操作甲献,每個(gè)線程+10000宰缤,理想結(jié)果應(yīng)該為200000,但是實(shí)際運(yùn)行結(jié)果卻小于這個(gè)值晃洒,而且結(jié)果每次都不一樣(可以多運(yùn)行幾次觀察):
這是為什么呢慨灭,程序中inc已經(jīng)加了volatile修飾,保證了線程的可見(jiàn)性球及,但是為什么結(jié)果還是會(huì)比預(yù)想的小呢氧骤?
這是因?yàn)?+操作并不是簡(jiǎn)單的一步操作,即他不是原子性的吃引,查看編譯后的字節(jié)碼文件筹陵,++的實(shí)際操作為:
(實(shí)事求是地說(shuō),使用字節(jié)碼來(lái)分析并發(fā)問(wèn)題仍然是不嚴(yán)謹(jǐn)?shù)哪鞒撸驗(yàn)榧词咕幾g出來(lái)只有一條字節(jié)碼指令惶翻,也并不意味執(zhí)行這條指令就是一個(gè)原子操作。一條字節(jié)碼指令在解釋執(zhí)行時(shí)鹅心,解釋器要運(yùn) 行許多行代碼才能實(shí)現(xiàn)它的語(yǔ)義吕粗。如果是編譯執(zhí)行,一條字節(jié)碼指令也可能轉(zhuǎn)化成若干條本地機(jī)器碼 指令旭愧。)
public void increase();
Code:
0: aload_0
1: dup
2: getfield #2 // Field inc:I
5: iconst_1
6: iadd
7: putfield #2 // Field inc:I
10: return
inc++操作分成了2.獲取字段 5.準(zhǔn)備常數(shù)1 6.進(jìn)行加1操作 7.賦值 四步
不保證原子性颅筋,即無(wú)法確保這四步操作不會(huì)被cpu切換打斷:
如圖cpu在線程1修改完之后還未寫(xiě)入內(nèi)存時(shí),切換到線程2输枯,執(zhí)行完了++操作议泵,此時(shí)cpu切換回線程1又把inc=1 寫(xiě)回去,造成了inc的值被覆蓋桃熄。
我們?cè)倏聪缕胀ǖ馁x值操作的字節(jié)碼文件 如:x=1
public void fun1(){
inc = 1;
}
// 編譯后
public void fun1();
Code:
0: aload_0
1: iconst_1
2: putfield #2 // Field inc:I
5: return
他沒(méi)有g(shù)etfield和add的操作先口,直接賦值,所以賦值操作算是原子性的瞳收。
而synchronized是如何保證原子性的呢碉京?
通過(guò)字節(jié)碼文件我們可以發(fā)現(xiàn),用synchronized修飾真的代碼塊在前后會(huì)執(zhí)行monitorenter和monitorexit指令螟深,這minitor指令底層則是通過(guò)lock和unlock來(lái)滿足原子性的谐宙,他只允許同時(shí)只有一個(gè)線程來(lái)操作資源。
推薦一篇很詳細(xì)很全面的文章界弧,此篇部分文字也有參考如下文章: