面試官最愛的volatile關(guān)鍵字_第1頁
面試官最愛的volatile關(guān)鍵字_第2頁
面試官最愛的volatile關(guān)鍵字_第3頁
面試官最愛的volatile關(guān)鍵字_第4頁
面試官最愛的volatile關(guān)鍵字_第5頁
已閱讀5頁,還剩5頁未讀, 繼續(xù)免費閱讀

下載本文檔

版權(quán)說明:本文檔由用戶提供并上傳,收益歸屬內(nèi)容提供方,若內(nèi)容存在侵權(quán),請進行舉報或認(rèn)領(lǐng)

文檔簡介

1、在Java相關(guān)的崗位面試中,很多面試官都喜歡考察面試者對Java并發(fā)的了解 程度,而以volatile關(guān)鍵字作為一個小的切入點,往往可以一問到底,把Java 內(nèi)存模型(JMM ) , Java并發(fā)編程的一些特性都牽扯出來,深入地話還可以 考察JVM底層實現(xiàn)以及操作系統(tǒng)的相關(guān)知識。下面我們以一次假想的面試過 程,來深入了解下volitile關(guān)鍵字吧!面試官:Java并發(fā)這塊了解的怎么樣?說說你對volatile關(guān)鍵字的理 解就我理解的而言,被volatile修飾的共享變量,就具有了以下兩點特性:1.保證了不同線程對該變量操作的內(nèi)存可見性;2.禁止指令重排序面試官:能不能詳細(xì)說下什么是內(nèi)存可見性,

2、什么又是重排序呢?這個聊起來可就多了,我還是從Java內(nèi)存模型說起吧。Java虛擬機規(guī)范試圖 定義一種Java內(nèi)存模型(JMM),來屏蔽掉各種硬件和操作系統(tǒng)的內(nèi)存訪問差 異,讓Java程序在各種平臺上都能到達(dá)一致的內(nèi)存訪問效果。簡單來說,由于 CPU執(zhí)行指令的速度是很快的,但是內(nèi)存訪問的速度就慢了很多,相差的不是 一個數(shù)量級,所以搞處理器的那群大佬們又在CPU里加了好幾層高速緩存。在Java內(nèi)存模型里,對上述的優(yōu)化又進行了一波抽象。JMM規(guī)定所有變量都 是存在主存中的,類似于上面提到的普通內(nèi)存,每個線程又包含自己的工作內(nèi) 存,方便理解就可以看成CPU上的寄存器或者高速緩存。所以線程的操作都是n

3、ew Thread () public void run() for (int j=0;j1) 保證前面的線程都執(zhí)行完Thread. yieldO ;System. out. println(test. inc);按道理來說結(jié)果是10000,但是運行下很可能是個小于10000的值。有人可 能會說volatile不是保證了可見性啊,一個線程對inc的修改,另外一個線程 應(yīng)該立刻看到??!可是這里的操作inc+是個復(fù)合操作啊,包括讀取inc的 值,對其自增,然后再寫回主存。假設(shè)線程A ,讀取了 inc的值為10 ,這時候被阻塞了,因為沒有對變量進行修 改,觸發(fā)不了 volatile規(guī)那么。線程B此時

4、也讀讀inc的值,主存里inc的值依舊為10,做自增,然后立刻就 被寫回主存了,為1L此時又輪到線程A執(zhí)行,由于工作內(nèi)存里保存的是10 ,所以繼續(xù)做自增,再 寫回主存,11又被寫了一遍。所以雖然兩個線程執(zhí)行了兩次increase。,結(jié)果 卻只加了一次。 有人說,volatile不是會使緩存行無效的嗎?但是這里線程A讀取到線程B也 進行操作之前,并沒有修改inc值,所以線程B讀取的時候,還是讀的10。又有人說,線程B將11寫回主存,不會把線程A的緩存行設(shè)為無效嗎?但是 線程A的讀取操作已經(jīng)做過了啊,只有在做讀取操作時,發(fā)現(xiàn)自己緩存行無 效,才會去讀主存的值,所以這里線程A只能繼續(xù)做自增了。綜上所

5、述,在這種復(fù)合操作的情景下,原子性的功能是維持不了了。但是 volatile在上面那種設(shè)置flag值的例子里,由于對flag的讀/寫操作都是單步 的,所以還是能保證原子性的。要想保證原子性,只能借助于synchronized,Lock以及并發(fā)包下的atomic的 原子操作類了,即對基本數(shù)據(jù)類型的自增(加1操作),自減(減1操 作)、以及加法操作(加一個數(shù)),減法操作(減一個數(shù))進行了封裝,保證 這些操作是原子性操作。面試官:說的還可以,那你知道volatile底層的實現(xiàn)機制?如果把加入volatile關(guān)鍵字的代碼和未加入volatile關(guān)鍵字的代碼都生成匯編代碼,會發(fā)現(xiàn)加入volatile關(guān)鍵

6、字的代碼會多出一個lock前綴指令。lock前綴指令實際相當(dāng)于一個內(nèi)存屏障,內(nèi)存屏障提供了以下功能:L重排序時不能把后面的指令重排序到內(nèi)存屏障之前的位置2.使得本CPU的Cache寫入內(nèi)存* *3.寫入動作也會引起別的CPU或者別的內(nèi)核無效化其Cache ,相當(dāng)于讓新寫入的值對別的線程可見。面試官:你在哪里會使用到volatile ,舉兩個例子呢?L狀態(tài)量標(biāo)記,就如上面對flag的標(biāo)記,我重新提一下:int a = 0;volatile bool flag = false;public void write() TOC o 1-5 h z a = 2;/Iflag = true;/2publi

7、c void multiply() if (flag) /3int ret = a * a;/4)這種對變量的讀寫操作,標(biāo)記為volatile可以保證修改對線程立刻可見。比 synchronized,Lock有一定的效率提升。2.單例模式的實現(xiàn),典型的雙重檢直鎖定(DCL )class Singleton private volatile static Singleton instance = null;private Singleton () public static Singleton getlnstance() if(instance=null) synchronized (Singl

8、eton, class) if(instance二二null)instance = new Singleton();return instance;這是一種懶漢的單例模式,使用時才創(chuàng)立對象,而且為了防止初始化操作的指令重排序,給instance加上了 volatile。以工作內(nèi)存為主,它們只能訪問自己的工作內(nèi)存,且工作前后都要把值在同步回主內(nèi)存。這么說得我自己都有些不清楚了,拿張紙畫一下:在線程執(zhí)行時,首先會從主存中read變量值,再load到工作內(nèi)存中的副本中,然后再傳給處理器執(zhí)行,執(zhí)行完畢后再給工作內(nèi)存中的副本賦值,隨后工 作內(nèi)存再把值傳回給主存,主存中的值才更新。使用工作內(nèi)存和主存,雖然

9、加快的速度,但是也帶來了一些問題。比方看下面 一個例子:i = i + 1;假設(shè)i初值為0 ,當(dāng)只有一個線程執(zhí)行它時,結(jié)果肯定得到1 ,當(dāng)兩個線程執(zhí)行 時,會得到結(jié)果2嗎?這倒不一定了。可能存在這種情況:線程 1: load i from 主存 i = 0i + 1 / i = 1線程2: load i from主存/因為線程1還沒將i的值寫回主存,所以i還是0i + 1 /i = 1線程1: save i to主存線程2: save i to主存如果兩個線程按照上面的執(zhí)行流程,那么i最后的值居然是1 了。如果最后的寫回生效的慢,你再讀取i的值,都可能是0 ,這就是緩存不一致問題。下面就要提到

10、你剛才問到的問題了,JMM主要就是圍繞著如何在并發(fā)過程中如 何處理原子性、可見性和有序性這3個特征來建立的,通過解決這三個問題, 可以解除緩存不一致的問題。而volatile跟可見性和有序性都有關(guān)。面試官:那你具體說說這三個特性呢?.原子性(Atomicity):Java中,對基本數(shù)據(jù)類型的讀取和賦值操作是原子性操作,所謂原子性操作就 是指這些操作是不可中斷的,要做一定做完,要么就沒有執(zhí)行。比方:i = 2; j = i; i+; i = i + 1;上面4個操作中,i=2是讀取操作,必定是原子性操作J = i你以為是原子性操 作,其實吧,分為兩步,一是讀取i的值,然后再賦值給j,這就是2步操

11、作 了,稱不上原子操作,i+和i=i + 1其實是等效的,讀取i的值,加1 ,再 寫回主存,那就是3步操作了。所以上面的舉例中,最后的值可能出現(xiàn)多種情 況,就是因為滿足不了原子性。這么說來,只有簡單的讀取,賦值是原子操作,還只能是用數(shù)字賦值,用變量 的話還多了一步讀取變量值的操作。有個例外是,虛擬機規(guī)范中允許對64位 數(shù)據(jù)類型(long和double),分為2次32為的操作來處理,但是最新JDK實 現(xiàn)還是實現(xiàn)了原子操作的。JMM只實現(xiàn)了基本的原子性,像上面i + +那樣的操作,必須借助于 synchronized和Lock來保證整塊代碼的原子性了。線程在釋放鎖之前,必然 會把i的值刷回到主存的

12、。.可見性(Visibility):說到可見性,Java就是利用volatile來提供可見性的。當(dāng)一個變量被volatile 修飾時,那么對它的修改會立刻刷新到主存,當(dāng)其它線程需要讀取該變量時, 會去內(nèi)存中讀取新值。而普通變量那么不能保證這一點。其實通過synchronized和Lock也能夠保證可見性,線程在釋放鎖之前,會把 共享變量值都刷回主存,但是synchronized和Lock的開銷都更大。.有序性(Ordering )JMM是允許編譯器和處理器對指令重排序的,但是規(guī)定了 as-if-serial語義, 即不管怎么重排序,程序的執(zhí)行結(jié)果不能改變。比方下面的程序段:double pi

13、= 3.14;/Adouble r = 1;/Bdouble s= pi * r * r;/C上面的語句,可以按照A-B-C執(zhí)行,結(jié)果為3.14,但是也可以按照 B-A-C的順序執(zhí)行,因為A、B是兩句獨立的語句,而C那么依賴于A、B , 所以A、B可以重排序,但是C卻不能排到A、B的前面。JMM保證了重排序 不會影響到單線程的執(zhí)行,但是在多線程中卻容易出問題。比方這樣的代碼:int a = 0;bool flag = false;publie void write() TOC o 1-5 h z a = 2;/Iflag = true;/2)public void multiply()if (

14、flag) /3int ret = a * a;4假如有兩個線程執(zhí)行上述代碼段,線程1先執(zhí)行write,隨后線程2再執(zhí)行multiply ,最后ret的值一定是4嗎?結(jié)果不一定:如下圖,write方法里的1和2做了重排序,線程1先對flag賦值為true , 隨后執(zhí)行到線程2 , ret直接計算出結(jié)果,再到線程1 ,這時候a才賦值為2彳艮 明顯遲了一步。這時候可以為flag加上volatile關(guān)鍵字,禁止重排序,可以確保程序的有序 性,也可以上重量級的synchronized和Lock來保證有序性,它們能保證那一 塊區(qū)域里的代碼都是一次性執(zhí)行完畢的。另外,JMM具備一些先天的有序性,即不需要通

15、過任何手段就可以保證的有序 性,通常稱為 happens-before 原那么。定義了如下 happens-before 規(guī)那么:1.程序順序規(guī)那么:一個線程中的每個操作,happens-before于該線程中的任 意后續(xù)操作2.監(jiān)視器鎖規(guī)那么:對一個線程的解鎖,happens-before于隨后對 這個線程的加鎖3.volatile變量規(guī)那么:對一個volatile域的寫,h叩pens- before于后續(xù)對這個volatile域的讀4.傳遞性:如果A happens-before B , 且 B happens-before C,那么 A happens-before C 5.start(

16、),JjHW:如果線 程A執(zhí)行操作ThreadB_start()(啟動線程B),那么A線程的 ThreadB_start()happens-before 于 B 中的任意操作 6.join()原那么:如果 A 執(zhí)行ThreadB.join。并且成功返回,那么線程B中的任意操作happens- before于線程A從ThreadB.join。操作成功返回。7.interrupt()原那么:對線 程interruptO方法的調(diào)用先行發(fā)生于被中斷線程代碼檢測到中斷事件的發(fā)生, 可以通過Terrupted()方法檢測是否有中斷發(fā)生8.finalize()原那么:- 個對象的初始化完成

17、先行發(fā)生于它的finalize。方法的開始第1條規(guī)那么程序順序規(guī)那么是說在一個線程里,所有的操作都是按順序的,但是 在JMM里其實只要執(zhí)行結(jié)果一樣,是允許重排序的,這邊的happens- before 強調(diào)的重點也是單線程執(zhí)行結(jié)果的正確性,但是無法保證多線程也是如 此。第2條規(guī)那么監(jiān)視器規(guī)那么其實也好理解,就是在加鎖之前,確定這個鎖之前已經(jīng) 被釋放了,才能繼續(xù)加鎖。第3條規(guī)那么,就適用到所討論的volatile ,如果一個線程先去寫一個變量,另 外一個線程再去讀,那么寫入操作一定在讀操作之前。第4條規(guī)那么,就是happens-before的傳遞性。后面幾條就不再一 一贅述了。面試官:volat

18、ile關(guān)鍵字如何滿足并發(fā)編程的三大特性的?那就要重提volatile變量規(guī)那么:對一個volatile域的寫,happens-before T 后續(xù)對這個volatile域的讀。這條再拎出來說,其實就是如果一個變量聲明成 是volatile的,那么當(dāng)我讀變量時,總是能讀到它的最新值,這里最新值是指 不管其它哪個線程對該變量做了寫操作,都會立刻被更新到主存里,我也能從 主存里讀到這個剛寫入的值。也就是說volatile關(guān)鍵字可以保證可見性以及有 序性。繼續(xù)拿上面的一段代碼舉例:int a = 0;bool flag = false;public void write() TOC o 1-5 h

19、z a = 2;/Iflag = true;/2)public void multiply() if (flag) /3int ret = a * a;/4這段代碼不僅僅受到重排序的困擾,即使L 2沒有重排序。3也不會那么順利 的執(zhí)行的。假設(shè)還是線程1先執(zhí)行write操作,線程2再執(zhí)行multiply操作, 由于線程1是在工作內(nèi)存里把flag賦值為1 ,不一定立刻寫回主存,所以線程 2執(zhí)行時,multiply再從主存讀flag值,仍然可能為false ,那么括號里的語 句將不會執(zhí)行。如果改成下面這樣:int a = 0;volatile bool flag = false;public void write() TOC o 1-5 h z a = 2;/Iflag 二 true;/2)

溫馨提示

  • 1. 本站所有資源如無特殊說明,都需要本地電腦安裝OFFICE2007和PDF閱讀器。圖紙軟件為CAD,CAXA,PROE,UG,SolidWorks等.壓縮文件請下載最新的WinRAR軟件解壓。
  • 2. 本站的文檔不包含任何第三方提供的附件圖紙等,如果需要附件,請聯(lián)系上傳者。文件的所有權(quán)益歸上傳用戶所有。
  • 3. 本站RAR壓縮包中若帶圖紙,網(wǎng)頁內(nèi)容里面會有圖紙預(yù)覽,若沒有圖紙預(yù)覽就沒有圖紙。
  • 4. 未經(jīng)權(quán)益所有人同意不得將文件中的內(nèi)容挪作商業(yè)或盈利用途。
  • 5. 人人文庫網(wǎng)僅提供信息存儲空間,僅對用戶上傳內(nèi)容的表現(xiàn)方式做保護處理,對用戶上傳分享的文檔內(nèi)容本身不做任何修改或編輯,并不能對任何下載內(nèi)容負(fù)責(zé)。
  • 6. 下載文件中如有侵權(quán)或不適當(dāng)內(nèi)容,請與我們聯(lián)系,我們立即糾正。
  • 7. 本站不保證下載資源的準(zhǔn)確性、安全性和完整性, 同時也不承擔(dān)用戶因使用這些下載資源對自己和他人造成任何形式的傷害或損失。

評論

0/150

提交評論