前言
本篇文章將從java內存模型、字節碼角度解讀volatile,因爲jvm屏蔽了系統、硬件的差異,所以從這個角度出發更直觀、更易理解;網上不乏從多核cpu多級緩存或cpu lock指令去解讀volatile的,私以爲這種解讀方式有問題,比如單核cpu存在內存可見性問題嗎?似乎沒有答案。再者,volatile爲什麼會防止指令重排?僅僅是因爲lock指令嗎,要知道lock是結果,原因是volatile的可見性及happens-before原則。在介紹volatile之前必須瞭解java內存模型。
java內存模型
java內存分爲主內存和線程本地內存(又稱爲緩存);主內存也稱爲堆內存,存儲靜態變量、實例數據、數組元素;在程序執行時,線程首先從主存copy變量到本地內存,修改完後,再將變量同步到主存。所以在多個線程共享同一塊主內存時,就存在使用過期數據問題。比如
public class TestVolatile {
static boolean shutdown = false;
//t1線程
static void m1(){
while(!shutdown){
}
System.out.println("shutdown...");
}
//t2線程
static void m2(){
shutdown = true;
System.out.println("shutdown 更改了");
}
public static void main(String[] args) throws InterruptedException {
new Thread(()->{
TestVolatile.m1();
},"t1").start();
//爲了讓t1充分的運行
Thread.sleep(1000);
//t2修改後,t1看不到volatile修改後的值
new Thread(()->{
TestVolatile.m2();
},"t2").start();
}
}
有兩個線程t1、t2,共享了靜態變量shutdown,它們分別從主內存copy了shutdown到本地內存,t1將shutdown更改爲true,同步到主存,t2看不到shutdown已經改了,仍然認爲shutdown爲false,所以t1線程永遠不會輸出shutdown;注:下圖中是t1修改了本地內存中的shutdown值,還未同步到主內存;
我們不禁會想,如果有辦法,在變量修改後,其他線程能立刻看到修改後的值,那就好了!很幸運,volatile就是幹這個事的!
volatile內存可見性
volatile[ˈvɒlətaɪl] 英文釋義易變的,volatile修飾的變量,修改後,對其它線程可見。那麼爲什麼volatile修飾的變量就對其它線程可見呢?將本例中的shutdown用volatile修飾,反編譯代碼javap -v TestVolatile
可看到shutdown多了一個acc_volatile
描述符。
static volatile boolean shutdown;
descriptor: Z
flags: ACC_STATIC, ACC_VOLATILE
查閱jvm字節碼指令可知,acc_volatile不允許變量緩存,這就是原因了!
ACC_VOLATILE Declared volatile; cannot be cached.
基於字節碼的解釋,t1直接操作主存中的shutdown,而非本地內存,而t2在使用shutdown時,也從主存取值而不再從本地內存,所以shutdown修改對t2可見。volatile的內存可見性明白了,指令重排又是怎麼回事呢?
volatile防止指令重排
相信讀者也發現了,很多介紹volatile的博文中,只提到volatile有防止指令重排作用,但究竟爲什麼volatile能防止指令重排,卻語焉不詳。
我們先說一下什麼是指令重排,比如以下代碼經過編譯器或者運行時都可能發生指令重排
//t1線程
int i = 1;
int j = 2;
變爲
//t1線程
int j = 2;
int i = 1;
這是被允許的,因爲i與j沒有依賴性,所以指令重排不會影響最終結果正確性。這在單線程環境下是沒問題的,但在多線程環境下會存在問題。比如
//t2線程
1、if(j==2){
2、 //認爲此時的i肯定等於1,從而進行一些操作,其實此處的i可能等於初始值0(假設i是成員變量)
}
那有什麼辦法可以防止指令重排?volatile!本例中,用volatile修改j
int i = 1;
volatile int j = 2;
那麼爲什麼volatile能防止指令重排呢?這就要說說java內存模型中的happens-before原則了,happens-before規定了代碼中的執行順序和內存可見性,happens-before原則有很多條,其中有一條叫程序次序規則(program order rule),說的是在單線程裏,書寫在前面的代碼happens-before書寫在後邊的代碼。比如,actionA happens-before actionB,那麼actionA對actionB是可見的。基於此,t1裏的volatile j=2
happens-before t2裏的if(j===2)
,又t1線程裏i=1
happens-before volatile j=2
,所以 i=1
happens-before if(j===2)
,所以i==1
對t2中的第2行代碼是可見的。正因爲如此,就保證了i==1
不能重排到j==2
後邊,所以防止了指令重排。從上可以看出指令重排是volatile內存可見性的副作用
。同理volatile後邊的語句,也不能指令重排到前邊。
這個地方要好好理解,最初t1中的i、j沒有依賴關係,所以可指令重排;但當t1中的j與t2中的j,產生了依賴後,導致了t1中的i與j也產生了依賴關係,所以i、j不能指令重排。
總結
volatile的作用
- 保證內存可見性,變量不能緩存,可以認爲線程直接修改主存中的變量、直接從主存中讀取變量;
- 防止指令重排序,這是由happens-before中的單線程內有序及volatile可見性衍生出的副作用
常見問題
- 哪些數據可以共享?
- 靜態變量、實例數據、數組元素。只要數據存在共享就會存在內存可見性問題。
- 哪些變量不存在共享?
- 局部變量、方法參數是線程私有,所以不存在數據共享問題
- 怎樣理解指令重排與程序次序規則?
- 如果認真思考一下,很容易想到,
int i=1; int j=1;
指令重排序與int i=1
happens-beforeint j=1
衝突吧?其實是不衝突的。happens-before
並不意味着代碼的執行順序,本例中int i=1
並一定在int j=1
之前執行,只要保證最終執行結果與happens-before的執行結果一致即可,以下引用java語言規範,在Stack Overflow也有類似提問。
- 如果認真思考一下,很容易想到,
It should be noted that the presence of a happens-before relationship between two actions does not necessarily imply that they have to take place in that order in an implementation. If the reordering produces results consistent with a legal execution, it is not illegal.
參考
When does java thread cache refresh happens?
loop doesnt see value changed by other thread without a printstatement
how to decompile volatile variable in java
instruction reordering happens before relationship in java
9年全棧工作經驗,歡迎關注個人公衆號