前幾天路過(guò)一個(gè)經(jīng)常負(fù)責(zé)面試的同事附近届搁,看到幾個(gè)人在討論volatile的可見(jiàn)性問(wèn)題氢橙,當(dāng)時(shí)第一感覺(jué)是 :“可見(jiàn)性還不簡(jiǎn)單嗎绅这?volatile修飾一個(gè)變量時(shí)纺蛆,那么在一個(gè)線程都對(duì)這個(gè)變量的更改,其他線程都立即可見(jiàn)污筷」す耄”
后面聽(tīng)到這樣一句話:“實(shí)際運(yùn)行結(jié)果能刷新你的三觀,網(wǎng)上的例子很多都是有問(wèn)題的”瓣蛀,讓我瞬間產(chǎn)生了興趣陆蟆。湊近一看,果然跟我的很多認(rèn)知都產(chǎn)生了偏差惋增。
為了解決其中的疑惑叠殷,查閱的不少文章,撥開(kāi)了一些迷霧诈皿,現(xiàn)將結(jié)果整理出來(lái)林束,與大家一同探討像棘。
基礎(chǔ)Java環(huán)境:
java version "1.8.0_172" Java(TM) SE Runtime Environment (build 1.8.0_172-b11) Java HotSpot(TM) 64-Bit Server VM (build 25.172-b11, mixed mode)
基本概念
Java內(nèi)存模型
首先先復(fù)習(xí)一下內(nèi)存模型的概念:
Java內(nèi)存模型(即Java Memory Model,簡(jiǎn)稱JMM)本身是一種抽象的概念壶冒,并不真實(shí)存在缕题,它描述的是一組規(guī)則或規(guī)范,通過(guò)這組規(guī)范 定義了程序中各個(gè)變量(包括實(shí)例字段依痊,靜態(tài)字段和構(gòu)成數(shù)組對(duì)象的元素)的訪問(wèn)方式。
JVM程序運(yùn)行的實(shí)體是線程怎披,而每個(gè)線程創(chuàng)建時(shí)JVM都會(huì)為其創(chuàng)建一個(gè)工作內(nèi)存(有些地方稱為椥剜遥空間),用于存儲(chǔ)線程私有的數(shù)據(jù)凉逛,而Java內(nèi)存模型中規(guī)定所有變量都存儲(chǔ)在主內(nèi)存性宏,主內(nèi)存是共享內(nèi)存區(qū)域,所有線程都可以訪問(wèn)状飞,但線程對(duì)變量的操作(讀取賦值等)必須在工作內(nèi)存中進(jìn)行毫胜,首先要將變量從主內(nèi)存拷貝的自己的工作內(nèi)存空間,然后對(duì)變量進(jìn)行操作诬辈,操作完成后再將變量寫(xiě)回主內(nèi)存酵使,不能直接操作主內(nèi)存中的變量,工作內(nèi)存中存儲(chǔ)著主內(nèi)存中的變量副本拷貝焙糟,前面說(shuō)過(guò)口渔,工作內(nèi)存是每個(gè)線程的私有數(shù)據(jù)區(qū)域,因此不同的線程間無(wú)法訪問(wèn)對(duì)方的工作內(nèi)存穿撮,線程間的通信(傳值)必須通過(guò)主內(nèi)存來(lái)完成缺脉,其簡(jiǎn)要訪問(wèn)過(guò)程如下圖:
volatile關(guān)鍵字
volatile是老生常談的一個(gè)關(guān)鍵字,大家在編程中其實(shí)用得都很少悦穿,面試中比較常見(jiàn)攻礼,也正是這個(gè)原因,讓大家對(duì)這一塊的理解與實(shí)際結(jié)果產(chǎn)生了偏差栗柒。
volatile是Java虛擬機(jī)提供的輕量級(jí)的同步機(jī)制礁扮。volatile關(guān)鍵字有如下兩個(gè)作用。
1)保證被volatile修飾的共享變量對(duì)所有線程 總是可見(jiàn)的瞬沦,也就是當(dāng)一個(gè)線程修改了一個(gè)被volatile修飾共享變量的值深员,新值總是可以被其他線程立即得知。
2)禁止指令重排序優(yōu)化蛙埂。
可見(jiàn)性
關(guān)于內(nèi)存模型和volatile的概念本篇不做詳細(xì)贅述倦畅,不熟悉的看官建議先百度一下。JMM是圍繞 原子性绣的、有序性叠赐、可見(jiàn)性 展開(kāi)的欲账,本文主要圍繞內(nèi)存模型的可見(jiàn)性出發(fā),通過(guò)實(shí)際例子來(lái)探究其運(yùn)行原理芭概。
先思考一個(gè)問(wèn)題:volatile保證的“立即可見(jiàn)”的反義是什么赛不?
這是大家最容易想到的答案,應(yīng)該是“不可見(jiàn)”罢洲,且有實(shí)實(shí)在在的例子讓我們覺(jué)得“不可見(jiàn)”深根不移踢故。
示例1:
package com.youzan;
/**
* Date: 2018/8/12
* @author xuzhiyi
*/
public class Test1 {
private static boolean flag = true;
private static int i = 0;
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
while (flag) {
i++;
}
System.out.printf("**********test1 跳出成功, i=%d **********\n", i);
});
thread.start();
Thread.sleep(100);
flag = false;
System.out.printf("**********test1 main thread 結(jié)束, i=%d **********\n", i);
}
}
示例2:
package com.youzan;
/**
* Date: 2018/8/12
* @author xuzhiyi
*/
public class Test2 {
private static volatile boolean flag = true;
private static int i = 0;
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
while (flag) {
i++;
}
System.out.printf("**********test2 跳出成功, i=%d **********\n", i);
});
thread.start();
Thread.sleep(100);
flag = false;
System.out.printf("**********test2 main thread 結(jié)束, i=%d **********\n", i);
}
}
示例1和示例2的唯一區(qū)別在于,示例2的flag有volatile修飾惹苗。上述示例的運(yùn)行結(jié)果大家都“知道”殿较,示例1會(huì)一直死循環(huán),示例2會(huì)立即跳出循環(huán)桩蓉。大家可能都運(yùn)行過(guò)這兩段(或者相似的)代碼淋纲,大部分人對(duì)結(jié)果很滿意,因?yàn)榉项A(yù)期院究,沒(méi)有加volatile關(guān)鍵字的成員變量多線程之間不可見(jiàn)洽瞬。
回到剛剛那個(gè)問(wèn)題,“立即可見(jiàn)”的反義是什么业汰?
通過(guò)上述實(shí)踐我們可以“肯定”的回答:“立即可見(jiàn)”的反義是“不可見(jiàn)”;锴浴!样漆!而且是“一直不可見(jiàn)”
說(shuō)到這里对供,可能有部分人有疑問(wèn)了,“立即可見(jiàn)”的反義應(yīng)該是“不立即可見(jiàn)”氛濒,說(shuō)人話就是“可能過(guò)一段時(shí)間后可見(jiàn)产场,不一定是馬上可見(jiàn)”∥韪停可是即使我們運(yùn)行一萬(wàn)遍示例1的代碼京景,都是一直不可見(jiàn)。怎么辦骗奖?繼續(xù)往下看确徙。
實(shí)戰(zhàn)
讓沒(méi)有volatile也能跳出循環(huán)
方式一
示例3:
package com.youzan;
/**
* Date: 2018/8/12
* @author xuzhiyi
*/
public class Test3 {
private static boolean flag = true;
private static int i = 0;
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
while (flag) {
i++;
}
System.out.printf("**********test3 跳出成功, i=%d **********\n", i);
});
thread.start();
Thread.sleep(1);
flag = false;
System.out.printf("**********test3 main thread 結(jié)束, i=%d **********\n", i);
}
}
在示例3中,我僅將示例1中的sleep時(shí)間改為1毫秒执桌,while循環(huán)即可成功跳出鄙皇,輸出結(jié)果如下:
**********test3 main thread 結(jié)束, i=60167 **********
**********test3 跳出成功, i=60167 **********
ps:主線程可能由于停頓時(shí)間太短,導(dǎo)致while循環(huán)根本沒(méi)進(jìn)去仰挣。重試幾次伴逸,當(dāng)i的值不為0即代表已經(jīng)進(jìn)入循環(huán)。
對(duì)比示例1和示例3我們可以得出一個(gè)結(jié)論:
- 當(dāng)主線程停頓時(shí)間很極短(1~2ms)時(shí)膘壶,可以跳出循環(huán)错蝴;
- 當(dāng)主線程停頓時(shí)間較長(zhǎng)時(shí)洲愤,無(wú)法跳出循環(huán);
結(jié)論變種1:
- 當(dāng)子線程循環(huán)執(zhí)行時(shí)間極短(1~2ms)時(shí)顷锰,可以跳出循環(huán)柬赐;
- 當(dāng)子線程循環(huán)執(zhí)行時(shí)間較長(zhǎng)時(shí),無(wú)法跳出循環(huán)官紫;
結(jié)論變種2:
- 當(dāng)子線程循環(huán)次數(shù)較少時(shí)肛宋,可以跳出循環(huán);
- 當(dāng)子線程循環(huán)次數(shù)較多時(shí)束世,無(wú)法跳出循環(huán)酝陈;
看上去是不是有點(diǎn)意思?代碼的執(zhí)行結(jié)果居然跟執(zhí)行時(shí)間良狈、循環(huán)次數(shù)有關(guān)后添?推斷到這里笨枯,有些看官可能已經(jīng)想到了JIT即使編譯優(yōu)化薪丁。沒(méi)錯(cuò),正是JIT的優(yōu)化對(duì)運(yùn)行結(jié)果產(chǎn)生了影響馅精。
關(guān)于JIT
當(dāng)虛擬機(jī)發(fā)現(xiàn)某個(gè)方法或代碼塊運(yùn)行特別頻繁時(shí)严嗜,就會(huì)把這些代碼認(rèn)定為“Hot Spot Code”(熱點(diǎn)代碼),為了提高熱點(diǎn)代碼的執(zhí)行效率洲敢,在運(yùn)行時(shí)漫玄,虛擬機(jī)將會(huì)把這些代碼編譯成與本地平臺(tái)相關(guān)的機(jī)器碼,并進(jìn)行各層次的優(yōu)化压彭,完成這項(xiàng)任務(wù)的正是 JIT 編譯器睦优。
運(yùn)行過(guò)程中會(huì)被即時(shí)編譯器編譯的“熱點(diǎn)代碼”有兩類:
1)被多次調(diào)用的方法。
2)被多次調(diào)用的循環(huán)體壮不。
如何驗(yàn)證上述結(jié)論呢汗盘?
- -Xint :強(qiáng)制使用解釋執(zhí)行的方式啟動(dòng)java虛擬機(jī),此模式下询一,不會(huì)使用JIT優(yōu)化隐孽,示例1和示例3的代碼都會(huì)跳出循環(huán)。
- -Xcomp:強(qiáng)制使用編譯執(zhí)行的方式啟動(dòng)java虛擬機(jī)健蕊,此模式下菱阵,代碼會(huì)被優(yōu)化并編譯成機(jī)器碼,示例1和示例3都無(wú)法填出循環(huán)缩功。
總結(jié)一下:mac下默認(rèn)為-Xmixed混合模式晴及,使用java -version可以查看,混合模式下只有熱點(diǎn)代碼達(dá)到一定閾值才會(huì)發(fā)生JIT優(yōu)化嫡锌,因此導(dǎo)致了上述看到的運(yùn)行時(shí)間長(zhǎng)短對(duì)運(yùn)行結(jié)果的影響抗俄。
方式二
不少熱心的網(wǎng)友在自己運(yùn)行示例1代碼的時(shí)候脆丁,會(huì)不由自主的加上一行print,如下:
示例4:
package com.youzan;
/**
* Date: 2018/8/12
* @author xuzhiyi
*/
public class Test4 {
private static boolean flag = true;
private static int i = 0;
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
while (flag) {
i++;
System.out.println("i=" + i);
}
System.out.printf("**********test4 跳出成功, i=%d **********\n", i);
});
thread.start();
Thread.sleep(100);
flag = false;
System.out.printf("**********test4 main thread 結(jié)束, i=%d **********\n", i);
}
}
上述代碼一運(yùn)行后成功跳出动雹,可能又驚倒了一批看官槽卫,為什么多一行print結(jié)果又不一樣了。而且就算在-Xcomp模式優(yōu)化后也可以跳出胰蝠。有點(diǎn)神奇吧歼培?
為了找出原因,我對(duì)print代碼進(jìn)行了幾次不同的替換:
示例5:
package com.youzan;
import java.util.HashMap;
/**
* Date: 2018/8/12
* @author xuzhiyi
*/
public class Test5 {
private static boolean flag = true;
private static int i = 0;
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
while (flag) {
doSomeThing1();
}
System.out.printf("**********test4 跳出成功, i=%d **********\n", i);
});
thread.start();
Thread.sleep(10);
flag = false;
System.out.printf("**********test4 main thread 結(jié)束, i=%d **********\n", i);
}
private static void doSomeThing1() {
System.out.println("doSomeThing1");
}
private static void doSomeThing2() {
synchronized (Test5.class) {
i++;
}
}
private static void doSomeThing3() {
i++;
Thread.yield();
}
private static void doSomeThing4() {
new HashMap<>();
}
}
上述代碼中茸塞,不論是在循環(huán)體內(nèi)執(zhí)行哪一個(gè)方法(doSomeThing1~ doSomeThing4
)躲庄,都可以正常跳出循環(huán)。為什么呢钾虐?究竟是什么影響了線程對(duì)成員變量的可見(jiàn)性呢噪窘?我的結(jié)論如下:
根據(jù)java的內(nèi)存模型規(guī)范,一個(gè)線程對(duì)普通變量的修改并不需要立即寫(xiě)回到主存效扫,且另一個(gè)線程讀取也不需要每一次都從主存中去讀取倔监。至于什么時(shí)候與主內(nèi)存同步,虛擬機(jī)只需保證方法出棧時(shí)將修改的值同步到主內(nèi)存菌仁。因此這其中有比較寬松的優(yōu)化空間浩习。而上述幾個(gè)方法,都存在一定的同步空間济丘。虛擬機(jī)會(huì)在此時(shí)與主內(nèi)存同步谱秽。
ps:以上結(jié)論純屬猜測(cè),沒(méi)有很好的論據(jù)摹迷,歡迎大家探討疟赊!
volatile的傳播范圍
思考兩個(gè)問(wèn)題:
- 把volatile對(duì)象傳遞給另一個(gè)對(duì)象,新對(duì)象是否立即可見(jiàn)呢峡碉?
- 當(dāng)volatile修飾對(duì)象時(shí)近哟,如果對(duì)象的嵌套的層級(jí)較深,那該對(duì)象的內(nèi)部是否立即可見(jiàn)呢异赫?
示例6:
package com.youzan;
/**
* Date: 2018/8/12
* @author xuzhiyi
*/
public class Test6 {
private static volatile ReferenceFlag referenceFlag = new ReferenceFlag();
private static int i = 0;
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
BaseFlag baseFlag = referenceFlag.baseFlag;
while (baseFlag.flag) {
i++;
}
System.out.printf("**********test6 跳出成功, i=%d **********\n", i);
});
thread.start();
Thread.sleep(100);
referenceFlag.baseFlag.flag = false;
System.out.printf("**********test6 main thread 結(jié)束, i=%d **********\n", i);
}
static class BaseFlag {
boolean flag = true;
}
static class ReferenceFlag {
volatile BaseFlag baseFlag = new BaseFlag();
}
}
在示例6中椅挣,使用了引用嵌套的方式來(lái)驗(yàn)證volatile是否可以傳遞給一個(gè)局部變量,示例中的引用都是用來(lái)volatile關(guān)鍵字來(lái)修飾塔拳,運(yùn)行結(jié)果是無(wú)法跳出鼠证。
結(jié)論一:當(dāng)使用一個(gè)變量來(lái)接受一個(gè)volatile修飾的變量時(shí),volatile的可見(jiàn)性并不會(huì)傳遞靠抑。即新的變量不再具有volatile特性量九。
示例2:
package com.youzan;
/**
* Date: 2018/8/12
* @author xuzhiyi
*/
public class Test7 {
private static int i = 0;
private static volatile DeapReferenceInnerFlag deapReferenceInnerFlag = new DeapReferenceInnerFlag();
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
while (deapReferenceInnerFlag.referenceInnerFlag.baseFlag.flag) {
i++;
}
System.out.printf("**********test7 跳出成功, i=%d **********\n", i);
});
thread.start();
Thread.sleep(100);
deapReferenceInnerFlag.referenceInnerFlag.baseFlag.flag = false;
System.out.printf("**********test7 main thread 結(jié)束, i=%d **********\n", i);
}
static class BaseFlag {
boolean flag = true;
}
static class ReferenceInnerFlag {
BaseFlag baseFlag = new BaseFlag();
}
static class DeapReferenceInnerFlag {
ReferenceInnerFlag referenceInnerFlag = new ReferenceInnerFlag();
}
}
示例7是一個(gè)多層嵌套的對(duì)象,只有最外層使用volatile修飾,當(dāng)其內(nèi)部的值改變后荠列,使用鏈?zhǔn)秸{(diào)用的方式类浪,則一直可以取到最新的值。
結(jié)論二:對(duì)于多層嵌套的對(duì)象肌似,最外層使用volatile修飾费就,使用鏈?zhǔn)秸{(diào)用的方式,volatile的可見(jiàn)性可以傳播川队。
ps:結(jié)論二沒(méi)有很好的理論依據(jù)力细,僅從實(shí)踐上看是如此。
總結(jié)
本篇結(jié)合實(shí)際的幾個(gè)例子固额,講述了幾個(gè)認(rèn)識(shí)誤區(qū)眠蚂。僅通過(guò)運(yùn)行結(jié)果說(shuō)明了一些問(wèn)題,但依然不夠深入斗躏,不足之處逝慧,還望指出。想深入探究的看官啄糙,可以參考下面的幾篇文章笛臣。