volatile關(guān)鍵字

前言

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


\color{red}{面試官:說一說你對Java中volatile關(guān)鍵字的理解?}
\color{green}{答:}就我理解的而言,被 volatile修飾的共享變量,具有了以下兩點特性:

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

\color{red}{面試官:能否詳細(xì)說一下什么是內(nèi)存可見性?什么又是指令重排序?}
\color{green}{答:}在Java保證多線程運(yùn)行安全,主要就是圍繞著如何在并發(fā)過程中如何處理原子性、可見性有序性這3個特征來建立的,通過解決這三個問題,可以解除緩存不一致的問題。

  • 原子性:提供互斥訪問,同一時刻只能有一個線程對數(shù)據(jù)進(jìn)行操作;
  • 可見性:一個線程對主內(nèi)存的修改可以及時地被其他線程看到;
  • 有序性:線程執(zhí)行代碼的順序是按照指令的先后順序執(zhí)行的。

volatile可見性有序性都有關(guān)。

注意:volatile關(guān)鍵字無法保證原子性。


\color{red}{面試官:那你具體說說這三個特性?}
\color{green}{答:}

  • 原子性(Atomicity)

原子性指的是一個或者多個操作在 CPU 執(zhí)行的過程中不被中斷的特性

\color{#C7254E}{[線程切換]帶來的原子性問題}

Java 并發(fā)程序都是基于多線程的,操作系統(tǒng)為了充分利用CPU的資源,將CPU分成若干個時間片,在多線程環(huán)境下,線程會被操作系統(tǒng)調(diào)度進(jìn)行任務(wù)切換。



為了直觀的了解什么是原子性,我們看下下面哪些操作是原子性操作

int count = 0;  //1
count++;  //2
int i = count;  //3

上面展示語句中,除了語句1是原子操作,其它兩個語句都不是原子性操作,下面我們來分析一下語句2
其實語句2在執(zhí)行的時候,包含三個指令操作

  • 指令 1:首先,需要把變量 count 從內(nèi)存加載到 CPU的寄存器
  • 指令 2:之后,在寄存器中執(zhí)行 +1 操作;
  • 指令 3:最后,將結(jié)果寫入內(nèi)存

對于上面的三條指令來說,如果線程 A 在指令 1 執(zhí)行完后做線程切換,線程 A 和線程 B 按照下圖的序列執(zhí)行,那么我們會發(fā)現(xiàn)兩個線程都執(zhí)行了 count+=1 的操作,但是得到的結(jié)果不是我們期望的 2,而是 1。


操作系統(tǒng)做任務(wù)切換,可能發(fā)生在任何一條CPU 指令執(zhí)行完時

  • 有序性(Ordering)

有序性指的是程序按照代碼的先后順序執(zhí)行

\color{#C7254E}{[編譯優(yōu)化]會帶來的有序性問題}

為了性能優(yōu)化,編譯器和處理器會進(jìn)行指令重排序,有時候會改變程序中語句的先后順序,比如程序:

      a = 5;      //1
      b = 20;     //2
      c = a + b;  //3

編譯器優(yōu)化后可能變成:

       b = 20;     //1
       a = 5;      //2
       c = a + b;  //3

在這個例子中,編譯器調(diào)整了語句的順序,但是不影響程序的最終結(jié)果

synchronized(具有有序性、原子性、可見性)表示鎖在同一時刻只能由一個線程進(jìn)行獲取,當(dāng)鎖被占用后,其他線程只能等待。

在單例模式的實現(xiàn)上有一種雙重檢驗鎖的方式

public class Singleton {

   private static volatile Singleton uniqueSingleton;

   private Singleton() {}

   public static Singleton getUniqueSingleton(){
       //先判斷對象是否已經(jīng)實例過,沒有實例化過才進(jìn)入加鎖代碼
       if (uniqueSingleton == null){
           //對Singleton對象加鎖
           synchronized (Singleton.class){
               if (uniqueSingleton ==null){
                   uniqueSingleton = new Singleton();
               }
           }
       }
       
       return uniqueSingleton;
   }
   
}

我們先看instance=newSingleton()的未被編譯器優(yōu)化的操作

  • 指令 1:分配一塊內(nèi)存 M;
  • 指令 2:在內(nèi)存 M 上初始化 Singleton對象;
  • 指令 3:然后 M 的地址賦值給 instance 變量。

編譯器優(yōu)化后的操作指令

  • 指令 1:分配一塊內(nèi)存 M;
  • 指令 2:將 M 的地址賦值給 instance 變量;
  • 指令 3:然后在內(nèi)存 M 上初始化 Singleton 對象。

現(xiàn)在有A,B兩個線程,我們假設(shè)線程A先執(zhí)行getInstance()方法,當(dāng)執(zhí)行編譯器優(yōu)化后的操作指令2時(此時候未完成對象的初始化),這時候發(fā)生了線程切換,那么線程B進(jìn)入,剛好執(zhí)行到第一次判斷 instance==null會發(fā)現(xiàn)instance不等于null了,所以直接返回instance,而此時的instance是沒有初始化過的。

  1. 可見性(Visibility)

可見性指的是當(dāng)一個線程修改了共享變量后,其他線程能夠立即感知修改后的值。

\color{#C7254E}{[緩存]導(dǎo)致的可見性問題}

首先我們來看一下Java內(nèi)存模型(JMM)


  • 我們定義的所有變量都儲存在主內(nèi)存中
  • 每個線程都有自己獨(dú)立的工作內(nèi)存,里面保存該線程使用到的變量的副本(主內(nèi)存中該變量的一份拷貝)
  • 線程對共享變量所有的操作都必須在自己的工作內(nèi)存中進(jìn)行,不能直接從主內(nèi)存中讀寫(不能越級)
  • 不同線程之間也無法直接訪問其他線程的工作內(nèi)存中的變量,線程間變量值的傳遞需要通過主內(nèi)存來進(jìn)行。(同級不能相互訪問)

共享變量可見性的實現(xiàn)原理:

線程1對共享變量的修改要被線程2及時看到的話,要經(jīng)過如下步驟:

  1. 線程1工作內(nèi)存1中更新的變量值刷新到主內(nèi)存
  2. 線程2主內(nèi)存中的變量的值更新到工作內(nèi)存2

\color{blue}{可以使用 synchronized 、volatile來保證可見性}


\color{red}{面試官:在哪里會用到volatile關(guān)鍵字呢?能否舉例說明}
\color{green}{答:}

  1. 狀態(tài)量標(biāo)記,比在COW機(jī)制中volatile Object[] array、AQS中的volatile int state變量;這種對變量的讀寫操作,標(biāo)記為volatile可以保證修改對線程立刻可見。比 synchronized,Lock有一定的效率提升。

  2. 雙重校驗鎖實現(xiàn)單例模式

public class Singleton {

    private static volatile Singleton uniqueSingleton;

    private Singleton() {}

    public static Singleton getUniqueSingleton(){
        //先判斷對象是否已經(jīng)實例過,沒有實例化過才進(jìn)入加鎖代碼
        if (uniqueSingleton == null){
            //對Singleton對象加鎖
            synchronized (Singleton.class){
                if (uniqueSingleton ==null){
                    uniqueSingleton = new Singleton();
                }
            }
        }
        
        return uniqueSingleton;
    }
    
}

注意,重點來了: uniqueInstance 采用 volatile關(guān)鍵字修飾也是很有必要。
uniqueInstance 采用 volatile關(guān)鍵字修飾也是很有必要的, uniqueInstance = new Singleton();這段代碼其實是分
為三步執(zhí)行:

  1. uniqueInstance分配內(nèi)存空間
  2. 初始化uniqueInstance
  3. uniqueInstance指向分配的內(nèi)存地址

但是由于 JVM 具有指令重排的特性,執(zhí)行順序有可能變成 1->3->2。指令重排在單線程環(huán)境下不會出先問題,但是在多線程環(huán)境下會導(dǎo)致一個線程獲得還沒有初始化的實例。例如,線程 T1 執(zhí)行了 1 和 3,此時 T2 調(diào)用getUniqueInstance() 后發(fā)現(xiàn)uniqueInstance 不為空,因此返回 uniqueInstance,但此時 uniqueInstance 還未被初始化。

這里使用的就是volatile 可以禁止 JVM 的指令重排,保證在多線程環(huán)境下也能正常運(yùn)行。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
禁止轉(zhuǎn)載,如需轉(zhuǎn)載請通過簡信或評論聯(lián)系作者。

友情鏈接更多精彩內(nèi)容