volatile
volatile關(guān)鍵字在Java面試中幾乎是必考題
單例模式的雙重檢查模式(DCL)一般會(huì)引申到synchronized關(guān)鍵字和volatile關(guān)鍵字
volatile關(guān)鍵字了解嗎?它保證了什么特性?用什么方式禁止指令重排的嗎?
1)volatile保證了什么特性
-
保證線程可見性(多線程環(huán)境下保證內(nèi)存可見性)
- 線程修改后的共享變量值能夠及時(shí)刷新,從工作內(nèi)存中刷新回主內(nèi)存;
- 其它線程能夠及時(shí)的把共享變量的值從主內(nèi)存中更新到自己的工作內(nèi)存中;
- Java 內(nèi)存模型規(guī)定,對(duì)于多個(gè)線程共享的變量,存儲(chǔ)在主內(nèi)存當(dāng)中,每個(gè)線程都有自己獨(dú)立的工作內(nèi)存,并且線程只能訪問自己的工作內(nèi)存,不可以訪問其它線程的工作內(nèi)存。工作內(nèi)存中保存了主內(nèi)存中共享變量的副本,線程要操作這些共享變量,只能通過操作工作內(nèi)存中的副本來實(shí)現(xiàn),操作完畢之后再同步回到主內(nèi)存當(dāng)中,其 JVM 模型大致如下圖。
- image-20210511121557443
-
禁止指令重排序優(yōu)化(多線程模式下禁止指令重排序優(yōu)化)
-
哪條代碼需要指令重排?
instance = new instance();
使用了volatile關(guān)鍵字之后,重排序被禁止,所有的寫操作(write)都發(fā)生在讀操作(read)之前。
-
用什么方式禁止指令重排的嗎?
內(nèi)存屏障:
讀屏障(Load Barrier):在讀指令前插入Load Barrier,可以讓高速緩存中的數(shù)據(jù)失效,強(qiáng)制重新從主內(nèi)存加載數(shù)據(jù),保證讀取的是最新數(shù)據(jù)。
寫屏障(Store Barrier):在寫指令后插入Store Barrier,能讓寫入緩存中的最新數(shù)據(jù)更新寫入主內(nèi)存,保證寫入的數(shù)據(jù)立刻對(duì)其他線程可見。
- 在工作內(nèi)存中,每次使用V(volatile變量)前都必須先從主內(nèi)存刷新最新的值,用于保證能看見其他線程對(duì)變量V所做的修改。
- 在工作內(nèi)存中,每次修改V后都必須立刻同步回主內(nèi)存中,用于保證其他線程可以看到自己對(duì)變量V所做的修改。
2)volatile導(dǎo)致哪條代碼需要指令重排?
instance = new Singleton();
會(huì)被編譯器編譯成如下JVM指令:
- memory = allocate(); //分配對(duì)象的內(nèi)存空間
- ctorInstance(memory); //初始化對(duì)象
- instance = memory; //設(shè)置instance指向剛分配的內(nèi)存空間
但是這些指令排序并非一成不變,有可能會(huì)經(jīng)過JVM和CPU的優(yōu)化,指令重排為如下順序:
- memory = allocate(); //分配對(duì)象的內(nèi)存空間
- instance = memory; //設(shè)置instance指向剛分配的內(nèi)存空間
- ctorInstance(memory); //初始化對(duì)象
當(dāng)線程A執(zhí)行完1,3,時(shí),instance對(duì)象還未完成初始化,但已經(jīng)不再指向null。此時(shí)如果線程B搶占到CPU資源,執(zhí)行 if(instance == null)的結(jié)果會(huì)是false,從而返回一個(gè)沒有初始化完成的instance對(duì)象。
實(shí)例化對(duì)象實(shí)際會(huì)分為3個(gè)步驟:
- 分配內(nèi)存空間
- 初始化對(duì)象
- 將對(duì)象指向剛分配的內(nèi)存空間
但有的編譯器由于性能的原因,可能會(huì)將第二步和第三步進(jìn)行重排序,順序就成了:
- 分配內(nèi)存空間
- 將對(duì)象指向剛分配的內(nèi)存空間
- 初始化對(duì)象

使用了volatile關(guān)鍵字之后,重排序被禁止,所有的寫操作(write)都發(fā)生在讀操作(read)之前。
