欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

詳解Java面試官最愛(ài)問(wèn)的volatile關(guān)鍵字

 更新時(shí)間:2018年01月20日 17:04:19   投稿:mengwei  
這篇文章主要介紹了詳解Java面試官最愛(ài)問(wèn)的volatile關(guān)鍵字,小編覺(jué)得還是挺不錯(cuò)的,具有一定借鑒價(jià)值,需要的朋友可以參考下

本文向大家分享的主要內(nèi)容是Java面試中一個(gè)常見(jiàn)的知識(shí)點(diǎn):volatile關(guān)鍵字。本文詳細(xì)介紹了volatile關(guān)鍵字的方方面面,希望大家在閱讀過(guò)本文之后,能完美解決volatile關(guān)鍵字的相關(guān)問(wèn)題。

 在Java相關(guān)的崗位面試中,很多面試官都喜歡考察面試者對(duì)Java并發(fā)的了解程度,而以volatile關(guān)鍵字作為一個(gè)小的切入點(diǎn),往往可以一問(wèn)到底,把Java內(nèi)存模型(JMM),Java并發(fā)編程的一些特性都牽扯出來(lái),深入地話還可以考察JVM底層實(shí)現(xiàn)以及操作系統(tǒng)的相關(guān)知識(shí)。 下面我們以一次假想的面試過(guò)程,來(lái)深入了解下volitile關(guān)鍵字吧!

面試官: Java并發(fā)這塊了解的怎么樣?說(shuō)說(shuō)你對(duì)volatile關(guān)鍵字的理解

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

1.保證了不同線程對(duì)該變量操作的內(nèi)存可見(jiàn)性;
2.禁止指令重排序

面試官: 能不能詳細(xì)說(shuō)下什么是內(nèi)存可見(jiàn)性,什么又是重排序呢?

這個(gè)聊起來(lái)可就多了,我還是從Java內(nèi)存模型說(shuō)起吧。 Java虛擬機(jī)規(guī)范試圖定義一種Java內(nèi)存模型(JMM),來(lái)屏蔽掉各種硬件和操作系統(tǒng)的內(nèi)存訪問(wèn)差異,讓Java程序在各種平臺(tái)上都能達(dá)到一致的內(nèi)存訪問(wèn)效果。簡(jiǎn)單來(lái)說(shuō),由于CPU執(zhí)行指令的速度是很快的,但是內(nèi)存訪問(wèn)的速度就慢了很多,相差的不是一個(gè)數(shù)量級(jí),所以搞處理器的那群大佬們又在CPU里加了好幾層高速緩存。 在Java內(nèi)存模型里,對(duì)上述的優(yōu)化又進(jìn)行了一波抽象。JMM規(guī)定所有變量都是存在主存中的,類似于上面提到的普通內(nèi)存,每個(gè)線程又包含自己的工作內(nèi)存,方便理解就可以看成CPU上的寄存器或者高速緩存。所以線程的操作都是以工作內(nèi)存為主,它們只能訪問(wèn)自己的工作內(nèi)存,且工作前后都要把值在同步回主內(nèi)存。 這么說(shuō)得我自己都有些不清楚了,拿張紙畫(huà)一下:

在線程執(zhí)行時(shí),首先會(huì)從主存中read變量值,再load到工作內(nèi)存中的副本中,然后再傳給處理器執(zhí)行,執(zhí)行完畢后再給工作內(nèi)存中的副本賦值,隨后工作內(nèi)存再把值傳回給主存,主存中的值才更新。 使用工作內(nèi)存和主存,雖然加快的速度,但是也帶來(lái)了一些問(wèn)題。比如看下面一個(gè)例子:

i = i + 1;

假設(shè)i初值為0,當(dāng)只有一個(gè)線程執(zhí)行它時(shí),結(jié)果肯定得到1,當(dāng)兩個(gè)線程執(zhí)行時(shí),會(huì)得到結(jié)果2嗎?這倒不一定了??赡艽嬖谶@種情況:

線程1: load i from 主存  // i = 0
    i + 1 // i = 1
線程2: load i from主存 // 因?yàn)榫€程1還沒(méi)將i的值寫回主存,所以i還是0
    i + 1 //i = 1
線程1: save i to 主存
線程2: save i to 主存

如果兩個(gè)線程按照上面的執(zhí)行流程,那么i最后的值居然是1了。如果最后的寫回生效的慢,你再讀取i的值,都可能是0,這就是緩存不一致問(wèn)題。 下面就要提到你剛才問(wèn)到的問(wèn)題了,JMM主要就是圍繞著如何在并發(fā)過(guò)程中如何處理原子性、可見(jiàn)性和有序性這3個(gè)特征來(lái)建立的,通過(guò)解決這三個(gè)問(wèn)題,可以解除緩存不一致的問(wèn)題。而volatile跟可見(jiàn)性和有序性都有關(guān)。

面試官:那你具體說(shuō)說(shuō)這三個(gè)特性呢?

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

i = 2;
j = i;
i++;
i = i + 1;

上面4個(gè)操作中,i=2是讀取操作,必定是原子性操作,j=i你以為是原子性操作,其實(shí)吧,分為兩步,一是讀取i的值,然后再賦值給j,這就是2步操作了,稱不上原子操作,i++和i = i + 1其實(shí)是等效的,讀取i的值,加1,再寫回主存,那就是3步操作了。所以上面的舉例中,最后的值可能出現(xiàn)多種情況,就是因?yàn)闈M足不了原子性。 這么說(shuō)來(lái),只有簡(jiǎn)單的讀取,賦值是原子操作,還只能是用數(shù)字賦值,用變量的話還多了一步讀取變量值的操作。有個(gè)例外是,虛擬機(jī)規(guī)范中允許對(duì)64位數(shù)據(jù)類型(long和double),分為2次32為的操作來(lái)處理,但是最新JDK實(shí)現(xiàn)還是實(shí)現(xiàn)了原子操作的。 JMM只實(shí)現(xiàn)了基本的原子性,像上面i++那樣的操作,必須借助于synchronized和Lock來(lái)保證整塊代碼的原子性了。線程在釋放鎖之前,必然會(huì)把i的值刷回到主存的。 2. 可見(jiàn)性(Visibility): 說(shuō)到可見(jiàn)性,Java就是利用volatile來(lái)提供可見(jiàn)性的。 當(dāng)一個(gè)變量被volatile修飾時(shí),那么對(duì)它的修改會(huì)立刻刷新到主存,當(dāng)其它線程需要讀取該變量時(shí),會(huì)去內(nèi)存中讀取新值。而普通變量則不能保證這一點(diǎn)。 其實(shí)通過(guò)synchronized和Lock也能夠保證可見(jiàn)性,線程在釋放鎖之前,會(huì)把共享變量值都刷回主存,但是synchronized和Lock的開(kāi)銷都更大。 3. 有序性(Ordering) JMM是允許編譯器和處理器對(duì)指令重排序的,但是規(guī)定了as-if-serial語(yǔ)義,即不管怎么重排序,程序的執(zhí)行結(jié)果不能改變。比如下面的程序段:

double pi = 3.14;  //A
double r = 1;    //B
double s= pi * r * r;//C

上面的語(yǔ)句,可以按照A->B->C執(zhí)行,結(jié)果為3.14,但是也可以按照B->A->C的順序執(zhí)行,因?yàn)锳、B是兩句獨(dú)立的語(yǔ)句,而C則依賴于A、B,所以A、B可以重排序,但是C卻不能排到A、B的前面。JMM保證了重排序不會(huì)影響到單線程的執(zhí)行,但是在多線程中卻容易出問(wèn)題。 比如這樣的代碼:

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
  }
}

假如有兩個(gè)線程執(zhí)行上述代碼段,線程1先執(zhí)行write,隨后線程2再執(zhí)行multiply,最后ret的值一定是4嗎?結(jié)果不一定:

如圖所示,write方法里的1和2做了重排序,線程1先對(duì)flag賦值為true,隨后執(zhí)行到線程2,ret直接計(jì)算出結(jié)果,再到線程1,這時(shí)候a才賦值為2,很明顯遲了一步。 這時(shí)候可以為flag加上volatile關(guān)鍵字,禁止重排序,可以確保程序的有序性,也可以上重量級(jí)的synchronized和Lock來(lái)保證有序性,它們能保證那一塊區(qū)域里的代碼都是一次性執(zhí)行完畢的。 另外,JMM具備一些先天的有序性,即不需要通過(guò)任何手段就可以保證的有序性,通常稱為happens-before原則。<<JSR-133:Java Memory Model and Thread Specification>>定義了如下happens-before規(guī)則: 1.程序順序規(guī)則: 一個(gè)線程中的每個(gè)操作,happens-before于該線程中的任意后續(xù)操作 2.監(jiān)視器鎖規(guī)則:對(duì)一個(gè)線程的解鎖,happens-before于隨后對(duì)這個(gè)線程的加鎖 3.volatile變量規(guī)則: 對(duì)一個(gè)volatile域的寫,happens-before于后續(xù)對(duì)這個(gè)volatile域的讀 4.傳遞性:如果A happens-before B ,且 B happens-before C, 那么 A happens-before C 5.start()規(guī)則: 如果線程A執(zhí)行操作ThreadB_start()(啟動(dòng)線程B) , 那么A線程的ThreadB_start()happens-before 于B中的任意操作 6.join()原則: 如果A執(zhí)行ThreadB.join()并且成功返回,那么線程B中的任意操作happens-before于線程A從ThreadB.join()操作成功返回。 7.interrupt()原則: 對(duì)線程interrupt()方法的調(diào)用先行發(fā)生于被中斷線程代碼檢測(cè)到中斷事件的發(fā)生,可以通過(guò)Thread.interrupted()方法檢測(cè)是否有中斷發(fā)生 8.finalize()原則:一個(gè)對(duì)象的初始化完成先行發(fā)生于它的finalize()方法的開(kāi)始 第1條規(guī)則程序順序規(guī)則是說(shuō)在一個(gè)線程里,所有的操作都是按順序的,但是在JMM里其實(shí)只要執(zhí)行結(jié)果一樣,是允許重排序的,這邊的happens-before強(qiáng)調(diào)的重點(diǎn)也是單線程執(zhí)行結(jié)果的正確性,但是無(wú)法保證多線程也是如此。 第2條規(guī)則監(jiān)視器規(guī)則其實(shí)也好理解,就是在加鎖之前,確定這個(gè)鎖之前已經(jīng)被釋放了,才能繼續(xù)加鎖。 第3條規(guī)則,就適用到所討論的volatile,如果一個(gè)線程先去寫一個(gè)變量,另外一個(gè)線程再去讀,那么寫入操作一定在讀操作之前。 第4條規(guī)則,就是happens-before的傳遞性。 后面幾條就不再一一贅述了。

面試官:volatile關(guān)鍵字如何滿足并發(fā)編程的三大特性的?

那就要重提volatile變量規(guī)則: 對(duì)一個(gè)volatile域的寫,happens-before于后續(xù)對(duì)這個(gè)volatile域的讀。 這條再拎出來(lái)說(shuō),其實(shí)就是如果一個(gè)變量聲明成是volatile的,那么當(dāng)我讀變量時(shí),總是能讀到它的最新值,這里最新值是指不管其它哪個(gè)線程對(duì)該變量做了寫操作,都會(huì)立刻被更新到主存里,我也能從主存里讀到這個(gè)剛寫入的值。也就是說(shuō)volatile關(guān)鍵字可以保證可見(jiàn)性以及有序性。 繼續(xù)拿上面的一段代碼舉例:

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、2沒(méi)有重排序。3也不會(huì)那么順利的執(zhí)行的。假設(shè)還是線程1先執(zhí)行write操作,線程2再執(zhí)行multiply操作,由于線程1是在工作內(nèi)存里把flag賦值為1,不一定立刻寫回主存,所以線程2執(zhí)行時(shí),multiply再?gòu)闹鞔孀xflag值,仍然可能為false,那么括號(hào)里的語(yǔ)句將不會(huì)執(zhí)行。 如果改成下面這樣:

int a = 0;
volatile bool flag = false;

public void write() {
  a = 2;       //1
  flag = true;    //2
}

public void multiply() {
  if (flag) {     //3
    int ret = a * a;//4
  }
}

那么線程1先執(zhí)行write,線程2再執(zhí)行multiply。根據(jù)happens-before原則,這個(gè)過(guò)程會(huì)滿足以下3類規(guī)則: 程序順序規(guī)則:1 happens-before 2; 3 happens-before 4; (volatile限制了指令重排序,所以1 在2 之前執(zhí)行) volatile規(guī)則:2 happens-before 3 傳遞性規(guī)則:1 happens-before 4 當(dāng)寫一個(gè)volatile變量時(shí),JMM會(huì)把該線程對(duì)應(yīng)的本地內(nèi)存中的共享變量刷新到主內(nèi)存 當(dāng)讀一個(gè)volatile變量時(shí),JMM會(huì)把該線程對(duì)應(yīng)的本地內(nèi)存置為無(wú)效,線程接下來(lái)將從主內(nèi)存中讀取共享變量。

面試官:volatile的兩點(diǎn)內(nèi)存語(yǔ)義能保證可見(jiàn)性和有序性,但是能保證原子性嗎?

首先我回答是不能保證原子性,要是說(shuō)能保證,也只是對(duì)單個(gè)volatile變量的讀/寫具有原子性,但是對(duì)于類似volatile++這樣的復(fù)合操作就無(wú)能為力了,比如下面的例子:

public class Test {
  public volatile int inc = 0;
 
  public void increase() {
    inc++;
  }
 
  public static void main(String[] args) {
    final Test test = new Test();
    for(int i=0;i<10;i++){
      new Thread(){
        public void run() {
          for(int j=0;j<1000;j++)
            test.increase();
        };
      }.start();
    }
 
    while(Thread.activeCount()>1) //保證前面的線程都執(zhí)行完
      Thread.yield();
    System.out.println(test.inc);
  }

按道理來(lái)說(shuō)結(jié)果是10000,但是運(yùn)行下很可能是個(gè)小于10000的值。有人可能會(huì)說(shuō)volatile不是保證了可見(jiàn)性啊,一個(gè)線程對(duì)inc的修改,另外一個(gè)線程應(yīng)該立刻看到??!可是這里的操作inc++是個(gè)復(fù)合操作啊,包括讀取inc的值,對(duì)其自增,然后再寫回主存。 假設(shè)線程A,讀取了inc的值為10,這時(shí)候被阻塞了,因?yàn)闆](méi)有對(duì)變量進(jìn)行修改,觸發(fā)不了volatile規(guī)則。 線程B此時(shí)也讀讀inc的值,主存里inc的值依舊為10,做自增,然后立刻就被寫回主存了,為11。 此時(shí)又輪到線程A執(zhí)行,由于工作內(nèi)存里保存的是10,所以繼續(xù)做自增,再寫回主存,11又被寫了一遍。所以雖然兩個(gè)線程執(zhí)行了兩次increase(),結(jié)果卻只加了一次。 有人說(shuō),volatile不是會(huì)使緩存行無(wú)效的嗎?但是這里線程A讀取到線程B也進(jìn)行操作之前,并沒(méi)有修改inc值,所以線程B讀取的時(shí)候,還是讀的10。 又有人說(shuō),線程B將11寫回主存,不會(huì)把線程A的緩存行設(shè)為無(wú)效嗎?但是線程A的讀取操作已經(jīng)做過(guò)了啊,只有在做讀取操作時(shí),發(fā)現(xiàn)自己緩存行無(wú)效,才會(huì)去讀主存的值,所以這里線程A只能繼續(xù)做自增了。 綜上所述,在這種復(fù)合操作的情景下,原子性的功能是維持不了了。但是volatile在上面那種設(shè)置flag值的例子里,由于對(duì)flag的讀/寫操作都是單步的,所以還是能保證原子性的。 要想保證原子性,只能借助于synchronized,Lock以及并發(fā)包下的atomic的原子操作類了,即對(duì)基本數(shù)據(jù)類型的 自增(加1操作),自減(減1操作)、以及加法操作(加一個(gè)數(shù)),減法操作(減一個(gè)數(shù))進(jìn)行了封裝,保證這些操作是原子性操作。

面試官:說(shuō)的還可以,那你知道volatile底層的實(shí)現(xiàn)機(jī)制?

如果把加入volatile關(guān)鍵字的代碼和未加入volatile關(guān)鍵字的代碼都生成匯編代碼,會(huì)發(fā)現(xiàn)加入volatile關(guān)鍵字的代碼會(huì)多出一個(gè)lock前綴指令。 lock前綴指令實(shí)際相當(dāng)于一個(gè)內(nèi)存屏障,內(nèi)存屏障提供了以下功能: 1.重排序時(shí)不能把后面的指令重排序到內(nèi)存屏障之前的位置 2.使得本CPU的Cache寫入內(nèi)存 ** **3.寫入動(dòng)作也會(huì)引起別的CPU或者別的內(nèi)核無(wú)效化其Cache,相當(dāng)于讓新寫入的值對(duì)別的線程可見(jiàn)。

面試官: 你在哪里會(huì)使用到volatile,舉兩個(gè)例子呢?

1.狀態(tài)量標(biāo)記,就如上面對(duì)flag的標(biāo)記,我重新提一下:

int a = 0;
volatile bool flag = false;

public void write() {
  a = 2;       //1
  flag = true;    //2
}

public void multiply() {
  if (flag) {     //3
    int ret = a * a;//4
  }
}

這種對(duì)變量的讀寫操作,標(biāo)記為volatile可以保證修改對(duì)線程立刻可見(jiàn)。比synchronized,Lock有一定的效率提升。 2.單例模式的實(shí)現(xiàn),典型的雙重檢查鎖定(DCL)

class Singleton{
  private volatile static Singleton instance = null;
 
  private Singleton() {
 
  }
 
  public static Singleton getInstance() {
    if(instance==null) {
      synchronized (Singleton.class) {
        if(instance==null)
          instance = new Singleton();
      }
    }
    return instance;
  }
}

這是一種懶漢的單例模式,使用時(shí)才創(chuàng)建對(duì)象,而且為了避免初始化操作的指令重排序,給instance加上了volatile。

總結(jié)

以上就是本文關(guān)于詳解Java面試官最愛(ài)問(wèn)的volatile關(guān)鍵字的全部?jī)?nèi)容,希望對(duì)大家有所幫助。感興趣的朋友可以繼續(xù)參閱本站其他相關(guān)專題,如有不足之處,歡迎留言指出。感謝朋友們對(duì)本站的支持!

相關(guān)文章

  • jpa?EntityManager?復(fù)雜查詢實(shí)例

    jpa?EntityManager?復(fù)雜查詢實(shí)例

    這篇文章主要介紹了jpa?EntityManager?復(fù)雜查詢實(shí)例,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2021-12-12
  • Java中的JWT使用詳解

    Java中的JWT使用詳解

    這篇文章主要介紹了Java中的JWT使用詳解,JWT是一個(gè)開(kāi)放標(biāo)準(zhǔn)(rfc7519),它定義了一種緊湊的、自包含的方式,用于在各方之間以JSON對(duì)象安全地傳輸信息,需要的朋友可以參考下
    2023-08-08
  • 如何利用泛型封裝通用的service層

    如何利用泛型封裝通用的service層

    這篇文章主要介紹了如何利用泛型封裝通用的service層,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2022-06-06
  • Java中Exception和Error的區(qū)別詳解

    Java中Exception和Error的區(qū)別詳解

    在 Java 開(kāi)發(fā)面試中,Exception 和 Error 的區(qū)別是一個(gè)經(jīng)典問(wèn)題,這個(gè)問(wèn)題不僅考察我們對(duì) Java 異常處理機(jī)制的理解,還考察我們?cè)趯?shí)際開(kāi)發(fā)中如何處理異常的能力,所以本文主要給大家介紹一下Java中Exception和Error的區(qū)別,需要的朋友可以參考下
    2025-04-04
  • SpringBoot部署到騰訊云的實(shí)現(xiàn)示例

    SpringBoot部署到騰訊云的實(shí)現(xiàn)示例

    記錄一下自己第一次部署springboot項(xiàng)目,本文主要介紹了SpringBoot部署到騰訊云的實(shí)現(xiàn)示例,對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧
    2023-08-08
  • SpringCloud集成Eureka并實(shí)現(xiàn)負(fù)載均衡的過(guò)程詳解

    SpringCloud集成Eureka并實(shí)現(xiàn)負(fù)載均衡的過(guò)程詳解

    這篇文章主要給大家詳細(xì)介紹了SpringCloud集成Eureka并實(shí)現(xiàn)負(fù)載均衡的過(guò)程,文章通過(guò)代碼示例和圖文講解的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作有一定的參考價(jià)值,需要的朋友可以參考下
    2023-11-11
  • Spring中Bean的三種實(shí)例化方式詳解

    Spring中Bean的三種實(shí)例化方式詳解

    這篇文章主要給大家介紹了關(guān)于Spring中實(shí)例化bean的三種方式:構(gòu)造方法、靜態(tài)工廠和實(shí)例工廠,對(duì)我們學(xué)習(xí)有一定的參考價(jià)值,需要的小伙伴可以了解一下
    2022-06-06
  • Java線程中synchronized的用法與原理解析

    Java線程中synchronized的用法與原理解析

    這篇文章主要介紹了Java線程中synchronized的用法與原理解析,只要有線程,就會(huì)有并發(fā)的現(xiàn)象,也同時(shí)會(huì)產(chǎn)生數(shù)據(jù)不一致,那么對(duì)于需要使用同一個(gè)數(shù)據(jù)的兩個(gè)線程,就會(huì)產(chǎn)生沖突,那么就引出了鎖的概念,本篇會(huì)針對(duì)性的說(shuō)下synchronized這個(gè)關(guān)鍵字,需要的朋友可以參考下
    2024-01-01
  • Java虛擬機(jī)JVM性能優(yōu)化(二):編譯器

    Java虛擬機(jī)JVM性能優(yōu)化(二):編譯器

    這篇文章主要介紹了Java虛擬機(jī)JVM性能優(yōu)化(二):編譯器,本文先是講解了不同種類的編譯器,并對(duì)客戶端編譯,服務(wù)器端編譯器和多層編譯的運(yùn)行性能進(jìn)行了對(duì)比,然后給出了幾種常見(jiàn)的JVM優(yōu)化方法,需要的朋友可以參考下
    2014-09-09
  • Java實(shí)現(xiàn)的文本字符串操作工具類實(shí)例【數(shù)據(jù)替換,加密解密操作】

    Java實(shí)現(xiàn)的文本字符串操作工具類實(shí)例【數(shù)據(jù)替換,加密解密操作】

    這篇文章主要介紹了Java實(shí)現(xiàn)的文本字符串操作工具類,可實(shí)現(xiàn)數(shù)據(jù)替換、加密解密等操作,涉及java字符串遍歷、編碼轉(zhuǎn)換、替換等相關(guān)操作技巧,需要的朋友可以參考下
    2017-10-10

最新評(píng)論