談談面試官最愛的volatile關鍵字

談談面試官最愛的volatile關鍵字

談談面試官最愛的volatile關鍵字 
在Java相關的崗位面試中,很多面試官都喜歡考察面試者對Java併發的瞭解程度,而以volatile關鍵字作爲一個小的切入點,往往可以一問到底,把Java內存模型(JMM),Java併發編程的一些特性都牽扯出來,深入地話還可以考察JVM底層實現以及操作系統的相關知識。

下面我們以一次假想的面試過程,來深入瞭解下volitile關鍵字吧!

面試官: Java併發這塊瞭解的怎麼樣?說說你對volatile關鍵字的理解

就我理解的而言,被volatile修飾的共享變量,就具有了以下兩點特性:

1 . 保證了不同線程對該變量操作的內存可見性;

2 . 禁止指令重排序

面試官: 能不能詳細說下什麼是內存可見性,什麼又是重排序呢?

這個聊起來可就多了,我還是從Java內存模型說起吧。

Java虛擬機規範試圖定義一種Java內存模型(JMM),來屏蔽掉各種硬件和操作系統的內存訪問差異,讓Java程序在各種平臺上都能達到一致的內存訪問效果。簡單來說,由於CPU執行指令的速度是很快的,但是內存訪問的速度就慢了很多,相差的不是一個數量級,所以搞處理器的那羣大佬們又在CPU里加了好幾層高速緩存。

在Java內存模型裏,對上述的優化又進行了一波抽象。JMM規定所有變量都是存在主存中的,類似於上面提到的普通內存,每個線程又包含自己的工作內存,方便理解就可以看成CPU上的寄存器或者高速緩存。所以線程的操作都是以工作內存爲主,它們只能訪問自己的工作內存,且工作前後都要把值在同步回主內存。

這麼說得我自己都有些不清楚了,拿張紙畫一下:

在線程執行時,首先會從主存中read變量值,再load到工作內存中的副本中,然後再傳給處理器執行,執行完畢後再給工作內存中的副本賦值,隨後工作內存再把值傳回給主存,主存中的值才更新。

使用工作內存和主存,雖然加快的速度,但是也帶來了一些問題。比如看下面一個例子:

i = i + 1; 
假設i初值爲0,當只有一個線程執行它時,結果肯定得到1,當兩個線程執行時,會得到結果2嗎?這倒不一定了。可能存在這種情況:

線程1: load i from 主存 // i = 0 i + 1 // i = 1 線程2: load i from主存 // 因爲線程1還沒將i的值寫回主存,所以i還是0 i + 1 //i = 1 線程1: save i to 主存 線程2: save i to 主存 
如果兩個線程按照上面的執行流程,那麼i最後的值居然是1了。如果最後的寫回生效的慢,你再讀取i的值,都可能是0,這就是緩存不一致問題。

下面就要提到你剛纔問到的問題了,JMM主要就是圍繞着如何在併發過程中如何處理原子性、可見性和有序性這3個特徵來建立的,通過解決這三個問題,可以解除緩存不一致的問題。而volatile跟可見性和有序性都有關。

面試官:那你具體說說這三個特性呢?

1 . 原子性(Atomicity): Java中,對基本數據類型的讀取和賦值操作是原子性操作,所謂原子性操作就是指這些操作是不可中斷的,要做一定做完,要麼就沒有執行。 比如:

i = 2; j = i; i++; i = i + 1; 
上面4個操作中,i=2是讀取操作,必定是原子性操作,j=i你以爲是原子性操作,其實吧,分爲兩步,一是讀取i的值,然後再賦值給j,這就是2步操作了,稱不上原子操作,i++和i = i + 1其實是等效的,讀取i的值,加1,再寫回主存,那就是3步操作了。所以上面的舉例中,最後的值可能出現多種情況,就是因爲滿足不了原子性。

這麼說來,只有簡單的讀取,賦值是原子操作,還只能是用數字賦值,用變量的話還多了一步讀取變量值的操作。有個例外是,虛擬機規範中允許對64位數據類型(long和double),分爲2次32爲的操作來處理,但是最新JDK實現還是實現了原子操作的。

JMM只實現了基本的原子性,像上面i++那樣的操作,必須藉助於synchronized和Lock來保證整塊代碼的原子性了。線程在釋放鎖之前,必然會把i的值刷回到主存的。

2 . 可見性(Visibility):

說到可見性,Java就是利用volatile來提供可見性的。 當一個變量被volatile修飾時,那麼對它的修改會立刻刷新到主存,當其它線程需要讀取該變量時,會去內存中讀取新值。而普通變量則不能保證這一點。

其實通過synchronized和Lock也能夠保證可見性,線程在釋放鎖之前,會把共享變量值都刷回主存,但是synchronized和Lock的開銷都更大。

3 . 有序性(Ordering)

JMM是允許編譯器和處理器對指令重排序的,但是規定了as-if-serial語義,即不管怎麼重排序,程序的執行結果不能改變。比如下面的程序段:

double pi = 3.14; //A double r = 1; //B double s= pi * r * r;//C 
上面的語句,可以按照A->B->C執行,結果爲3.14,但是也可以按照B->A->C的順序執行,因爲A、B是兩句獨立的語句,而C則依賴於A、B,所以A、B可以重排序,但是C卻不能排到A、B的前面。JMM保證了重排序不會影響到單線程的執行,但是在多線程中卻容易出問題。

比如這樣的代碼:

int a = 0; bool flag = false; public void write() { a = 2; //1 flag = true; //2 } public void multiply() { if (flag) { //3 int ret = a * a;//4 } } 
假如有兩個線程執行上述代碼段,線程1先執行write,隨後線程2再執行multiply,最後ret的值一定是4嗎?結果不一定:

如圖所示,write方法裏的1和2做了重排序,線程1先對flag賦值爲true,隨後執行到線程2,ret直接計算出結果,再到線程1,這時候a才賦值爲2,很明顯遲了一步。

這時候可以爲flag加上volatile關鍵字,禁止重排序,可以確保程序的“有序性”,也可以上重量級的synchronized和Lock來保證有序性,它們能保證那一塊區域裏的代碼都是一次性執行完畢的。

另外,JMM具備一些先天的有序性,即不需要通過任何手段就可以保證的有序性,通常稱爲happens-before原則。<

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章