Android并發(fā):輕松掌握Volatile與Synchronized

前言

對(duì)Android開(kāi)發(fā)者來(lái)說(shuō),相信對(duì)并發(fā)編程知識(shí)的掌握是非常薄弱的,一直是個(gè)人進(jìn)階的軟肋之一。對(duì)于并發(fā)實(shí)踐經(jīng)驗(yàn)缺乏的開(kāi)發(fā)者來(lái)說(shuō),文縐縐的技術(shù)書(shū)籍和博客,會(huì)比較羞澀難懂。從本文開(kāi)始,嘗試著逐個(gè)攻破并發(fā)編程的基礎(chǔ)知識(shí)點(diǎn)。

由于無(wú)知與惰性,讓我們感覺(jué)摸到了技術(shù)的天花板!

面試10問(wèn)

本文結(jié)合個(gè)人實(shí)際面試經(jīng)驗(yàn)和最近學(xué)習(xí)歸納總結(jié)而出,歡迎各位大佬點(diǎn)贊支持。

通過(guò)面試10問(wèn),讓大家掌握單例模式雙重檢查模式靜態(tài)內(nèi)部類單例模式,并了解其中原理。從原理進(jìn)而引出本文的重點(diǎn):volatilesynchronized。

第1問(wèn):平常在Android開(kāi)發(fā)中,有用到哪么設(shè)計(jì)模式么?

當(dāng)時(shí)回答:平常用的比較多的是單例模式、構(gòu)造者模式、工廠模式。尤其是單例模式中雙重檢查模式和靜態(tài)類單例模式;能夠保證多線程對(duì)象唯一,不會(huì)創(chuàng)建多個(gè)實(shí)例導(dǎo)致程序執(zhí)行錯(cuò)誤或影響性能。

解讀:雖然設(shè)計(jì)模式有很多種,個(gè)人來(lái)說(shuō),經(jīng)常用也就單例模式了。雖然面試前突擊瀏覽復(fù)習(xí)了,然面試一緊張,沒(méi)啥卵用。所以回答一定要往自己了解的說(shuō),并引導(dǎo)面試官往自己會(huì)的問(wèn)。

第2問(wèn):在紙上寫(xiě)一下雙重檢查模式和靜態(tài)類單例模式代碼?

心理活動(dòng):還好面試前自己已經(jīng)默寫(xiě)過(guò)很多遍了,問(wèn)題不大,嘩啦啦的寫(xiě)出來(lái):

雙重檢查模式:

public class Singleton {  
    private volatile static Singleton singleton;  
    private Singleton (){}  
    public static Singleton getSingleton() {  
    if (singleton == null) {  
        synchronized (Singleton.class) {  
        if (singleton == null) {  
            singleton = new Singleton(); 
             }  
        }  
    }  
    return singleton;  
    }  
}
復(fù)制代碼

靜態(tài)內(nèi)部類模式:

public class Singleton { 
    private Singleton(){
    }
      public static Singleton getSingleton(){  
        return Inner.instance;  
    }  
    private static class Inner {  
        private static final Singleton instance = new Singleton();  
    }  
} 

復(fù)制代碼

寫(xiě)好遞給面試官:雙重檢查模式和單例模式都能夠有效保證線程安全,又都是延時(shí)初始化,能夠減少不必要的性能開(kāi)銷。

第3問(wèn):雙重檢查模式有什么需要注意地方?

public class Singleton {
    private volatile static Singleton singleton;

    private Singleton() { }  //1

    public static Singleton getSingleton() {   //2
        if (singleton == null) {  // 3.1
            synchronized (Singleton.class) {  //3.2
                if (singleton == null) {  //3.3
                    singleton = new Singleton(); //4 
                }
            }
        }
        return singleton;
    }
}
復(fù)制代碼

答:雙重檢查模式需要注意以下幾點(diǎn):

  1. 構(gòu)造函數(shù)得私有,禁止其他對(duì)象直接創(chuàng)建實(shí)例;
  2. 對(duì)外提供一個(gè)靜態(tài)方法,可以獲取唯一的實(shí)例;
  3. 即然是雙重檢查模式,就意味著創(chuàng)建實(shí)例過(guò)程會(huì)有兩層檢查。第一層就是最外層的判空語(yǔ)句:代碼3.1處的if (singleton == null),該判斷沒(méi)有加鎖處理,避免第一次檢查singleton對(duì)象非null時(shí),多線程加鎖和初始化操作;當(dāng)前對(duì)象未創(chuàng)建時(shí),通過(guò)synchronized關(guān)鍵字同步代碼塊,持有當(dāng)前Singleton.class的鎖,保證線程安全,然后進(jìn)行第二次檢查。
  4. Singleton類持有的singleton實(shí)例引用需要volatile關(guān)鍵字修飾,因?yàn)樵谧詈笠徊?code>singleton = new Singleton(); 創(chuàng)建實(shí)例的時(shí)候可能會(huì)重排序,導(dǎo)致singleton對(duì)象逸出,導(dǎo)致其他線程獲取到一個(gè)未初始化完畢的對(duì)象。

第4問(wèn):剛剛講到的第四點(diǎn),為什么會(huì)有重排序,volatile關(guān)鍵字如何禁止重排序?

答:重排序是指編輯器和處理器為了優(yōu)化程序性能而對(duì)指令序列進(jìn)行重排序的一種手段。只要遵守as -if-serial語(yǔ)義(無(wú)論怎么重排序,單線程程序的執(zhí)行結(jié)果不會(huì)改變)。所以編譯器為了優(yōu)化性能,可能會(huì)對(duì)下圖中2和3步驟進(jìn)行重排序,這種重排序時(shí)允許的,因?yàn)椴粫?huì)改變單線程(目前只有該線程獨(dú)占該代碼塊)內(nèi)程序的執(zhí)行結(jié)果。

在單線程環(huán)境是沒(méi)有問(wèn)題,如果在多線程環(huán)境下,程序的執(zhí)行結(jié)果就會(huì)被破壞。如下圖所示,線程B在第一步判空時(shí),singleton實(shí)例的引用已經(jīng)非null,所以它不進(jìn)入申請(qǐng)鎖階段,而直接訪問(wèn)對(duì)象,但此對(duì)象還沒(méi)初始化完成,那么對(duì)象在實(shí)際使用就會(huì)出各種問(wèn)題。

volatile修飾的變量本身具有可見(jiàn)性和原子性,所謂的可見(jiàn)性是指對(duì)一個(gè)volatile變量的讀值,讀到的值是所有線程中最新修改的值;而原子性是指對(duì)單個(gè)變量的讀寫(xiě)具有原子性。之所以會(huì)有這兩個(gè)特性,是因?yàn)闀?huì)在該共享變量的匯編指令之前增加Lock指令,該Lock前綴指令會(huì)在多核處理器做兩件事:

1、將當(dāng)前處理器緩存行的數(shù)據(jù)寫(xiě)回到系統(tǒng)內(nèi)存;

2、這個(gè)寫(xiě)回內(nèi)存的操作會(huì)使其他處理器里緩存了該內(nèi)存地址的數(shù)據(jù)無(wú)效。

ps:?jiǎn)魏颂幚砥饕粫r(shí)刻只能有一條線程執(zhí)行,多線程是指單核CPU對(duì)不同線程進(jìn)行上下文切換和調(diào)度;多核處理器同一個(gè)時(shí)刻可能多條線程(每個(gè)核一條線程)并發(fā)執(zhí)行, 這時(shí)同步非常重要,現(xiàn)代CPU基本都是多核了。

由于volatie變量的可見(jiàn)性這個(gè)特性使其 寫(xiě)-讀 建立起了happens-before關(guān)系,從內(nèi)存語(yǔ)義的角度上說(shuō),線程A寫(xiě)一個(gè)volatile變量,實(shí)質(zhì)上是線程A向接下來(lái)將要讀這個(gè)volatiel變量的某個(gè)線程發(fā)出了通知。原理上講的話,在寫(xiě)一個(gè)volatile變量是,JAVA內(nèi)存模型(JMM)會(huì)把該線程對(duì)應(yīng)的本地內(nèi)存中的共享變量刷新到主內(nèi)存;而在讀volatile變量時(shí),會(huì)把該線程對(duì)應(yīng)的本地內(nèi)存置為無(wú)效,從主內(nèi)存中讀取該變量。線程之間通過(guò)共享程序的volatile變量(共享狀態(tài)),通過(guò)寫(xiě)讀操作共享狀態(tài)進(jìn)行隱式通信。

JMM為了實(shí)現(xiàn)這種volatile內(nèi)存語(yǔ)義,會(huì)限制編譯器和處理器的部分重排序。

為編譯器優(yōu)化制定以下三條規(guī)則

  • 第一個(gè)操作是對(duì)volatile變量的讀,無(wú)論第二個(gè)操作是什么,都禁止重排序;
  • 第一個(gè)操作是對(duì)volatile變量的寫(xiě),第二個(gè)操作是對(duì)volatile的讀,禁止重排序;
  • 第二個(gè)操作是對(duì)volatile變量的寫(xiě),無(wú)論第一個(gè)操作是什么,都禁止重排序;

從第2條規(guī)則就可以理解通過(guò)添加volatile關(guān)鍵字修飾單例的引用,可以禁止重排序。

根據(jù)這三條規(guī)則,編譯器會(huì)在生成字節(jié)碼時(shí),在指令序列插入適當(dāng)?shù)?,保守策略的?nèi)存屏障(一組CPU指令,實(shí)現(xiàn)對(duì)內(nèi)存操作的順序限制)。

  • volatile寫(xiě)操作前插入StoreStore屏障;
  • volatile寫(xiě)操作后插入StoreLoad屏障;
  • volatile讀操作后插入LoadLoad屏障;
  • volatile讀操作后插入LoadStore屏障;

以上內(nèi)存屏障時(shí)非常保守,編譯器在生成字節(jié)碼時(shí),也會(huì)進(jìn)行部分優(yōu)化,減少一些不必要的內(nèi)存屏障,以提高性能。不同的處理器會(huì)根據(jù)自身的內(nèi)存模型繼續(xù)優(yōu)化。

ps:JMM是為了屏蔽底層硬件內(nèi)存模型不一致,為頂層開(kāi)發(fā)提供一套標(biāo)準(zhǔn)的內(nèi)存模型,讓開(kāi)發(fā)這專注要業(yè)務(wù)開(kāi)發(fā)。

第5問(wèn):剛剛提到的happens-before規(guī)則,具體怎么說(shuō)來(lái)的?

答:從JDK5開(kāi)始,使用了新的JSR-133內(nèi)存模型,該模型定義了happens-before 規(guī)則:

  1. 程序順序規(guī)則:一個(gè)線程中的每個(gè)操作,happens-before于該線程的任意后續(xù)操作;
  2. 監(jiān)視器原則:對(duì)一個(gè)鎖的解鎖,happens-before于隨后對(duì)該鎖的加鎖;
  3. volatile規(guī)則:對(duì)一個(gè)volatile變量的寫(xiě),happens-before 于任意后續(xù)對(duì)這個(gè)volatile域的讀;
  4. 傳遞性:如果A happes-before B,B happens-before C,那么A happens-before C;
  5. start()原則:線程A執(zhí)行ThreadB.start()操作,start() happens-before 線程B內(nèi)所有操作;
  6. jion()原則:如果線程A執(zhí)行 ThreadB.jion()并成功返回,那線程B的所有操作都happens-before 于A從jion()操作成功返回。

第6問(wèn):規(guī)則第2點(diǎn)講到了鎖,那鎖在雙重檢查單例模式起了什么作用?

答:在代碼3.2處,用到了synchronized 關(guān)鍵字,對(duì)Singletion.Class對(duì)象進(jìn)行了同步,確保了在多線程環(huán)境下只有一個(gè)線程對(duì)Singletion類的Class對(duì)象進(jìn)行實(shí)例化。在Java中,每一個(gè)對(duì)象都可以作為鎖:

  1. 對(duì)于普通同步方法,鎖是當(dāng)前實(shí)例對(duì)象;
  2. 對(duì)于靜態(tài)同步方法,鎖是當(dāng)前類的Class對(duì)象;
  3. 對(duì)于同步方法塊,鎖是Synchoized括號(hào)的Class對(duì)象。

第7問(wèn):靜態(tài)內(nèi)部類單例模式有沒(méi)有用到鎖?

答:有的,JVM在類的初始化階段(在Class被加載后,且在線程使用之前),會(huì)執(zhí)行類的初始化,JVM會(huì)去獲取一個(gè)鎖,這個(gè)鎖能同步多個(gè)線程對(duì)同一個(gè)類的初始化。

當(dāng)一個(gè)線程A獲取到這個(gè)初始化鎖時(shí),其他線程想要獲取初始化鎖只能等待;線程A執(zhí)行類靜態(tài)初始化和初始化靜態(tài)字段的過(guò)程,就算發(fā)生類似雙重檢查模式的重排序,對(duì)結(jié)果也沒(méi)有影響,因?yàn)榇藭r(shí)沒(méi)有其他線程可以捕獲到初始化鎖。線程A初始化完畢,釋放鎖并通知等待獲取初始化鎖的線程。根據(jù)happens-befroe關(guān)系中的監(jiān)視器規(guī)則,當(dāng)其他線程獲取到初始鎖時(shí),已經(jīng)能看到線程A的初始化所有操作,此時(shí)靜態(tài)對(duì)象已經(jīng)初始化完畢,其他線程無(wú)需再初始化。

第8問(wèn):了解過(guò)鎖的原理,知道鎖存儲(chǔ)在哪么?

答:JVM(Java虛擬機(jī))是基于進(jìn)入和退出Monitor對(duì)象來(lái)實(shí)現(xiàn)方法同步和代碼塊同步的。同步代碼塊使用monitorenter指令在編譯后插入到同步代碼塊的開(kāi)始位置,使用monitorexit插入到同步代碼塊的結(jié)束處或異常處,monitorenter必須有對(duì)應(yīng)monitorexit指令與之配對(duì)。任何對(duì)象都有一個(gè)monitor與之相關(guān)聯(lián),當(dāng)且一個(gè)monitor被持有后,將處于鎖定狀態(tài)。線程執(zhí)行到monitorenter指令時(shí),將會(huì)嘗試獲取對(duì)象所對(duì)應(yīng)的monitor的所有權(quán),即獲得對(duì)象的鎖。方法則是在方法的指令前增加ACC_SYNCHRONIZED修飾符。

Synchronized用的鎖是存放在Java的對(duì)象頭;如果對(duì)象是數(shù)組,用3字寬存儲(chǔ)對(duì)象頭,其中一字寬用于存儲(chǔ)數(shù)組長(zhǎng)度;非數(shù)組,則2字寬存儲(chǔ)對(duì)象頭。在32位虛擬機(jī),1字寬=4字節(jié)=32位。

第9問(wèn):即然了解過(guò)Java的對(duì)象頭,那應(yīng)該清楚鎖升級(jí)的幾種狀態(tài)吧,說(shuō)一下?

答:在Java SE6,為了減少獲得鎖和釋放鎖帶來(lái)的性能消耗,引入了偏向鎖輕量級(jí)鎖。意味著此時(shí)鎖從低到高共有四種狀態(tài):無(wú)鎖狀態(tài)、偏向鎖狀態(tài)、輕量級(jí)鎖狀態(tài)和重量級(jí)鎖狀態(tài)。鎖的狀態(tài)是根據(jù)線程對(duì)鎖的競(jìng)爭(zhēng)情況來(lái)定義的。32位JVM運(yùn)行狀態(tài)下,Mark Work的存儲(chǔ)結(jié)構(gòu):

偏向鎖: 線程在大多數(shù)情況下并不存在競(jìng)爭(zhēng)條件,使用同步會(huì)消耗性能,而偏向鎖是對(duì)鎖的優(yōu)化,可以消除同步,提升性能。當(dāng)一個(gè)線程獲得鎖,會(huì)將對(duì)象頭的鎖標(biāo)志位設(shè)為01,進(jìn)入偏向模式.偏向鎖可以在讓一個(gè)線程一直持有鎖,在其他線程需要競(jìng)爭(zhēng)鎖的時(shí)候,再釋放鎖。==》只有一個(gè)線程進(jìn)入臨界區(qū)。

輕量級(jí)鎖: 當(dāng)線程A獲得偏向鎖后,線程B進(jìn)入競(jìng)爭(zhēng)狀態(tài),需要獲得線程A持有的鎖,那么線程A撤銷偏向鎖,進(jìn)入無(wú)鎖狀態(tài)。線程A和線程B交替進(jìn)入臨界區(qū),偏向鎖無(wú)法滿足,膨脹到輕量級(jí)鎖,鎖標(biāo)志位設(shè)為00。==》多個(gè)線程交替進(jìn)入臨界區(qū)。

重量級(jí)鎖: 當(dāng)多線程交替進(jìn)入臨界區(qū),輕量級(jí)鎖hold得住。但如果多個(gè)線程同時(shí)進(jìn)入臨界區(qū),hold不住了,膨脹到重量級(jí)鎖==》多個(gè)線程同時(shí)進(jìn)入臨界區(qū)。

第10問(wèn):為什么Synchronized夠用,還要增加Volatile?

Volatile相對(duì)Synchronized來(lái)說(shuō)在同步上比較輕量級(jí),能夠有效降低CPU頻繁的線程上下文切換和調(diào)度。同時(shí),Volatile的原子操作是針對(duì)單個(gè)volatile變量的寫(xiě)讀操作,無(wú)法和Sychronized對(duì)整個(gè)方法或代碼塊起的作用相比較。

總結(jié)

基本每一問(wèn)都會(huì)涉及到一些知識(shí)點(diǎn),面試官也會(huì)從不同方向去提問(wèn),引出不同知識(shí)點(diǎn)。例如后面幾個(gè)問(wèn)題可以引出Java的內(nèi)存模型,這些都是面試的高頻問(wèn)題。

通過(guò)本文,需要掌握單例模式的兩種寫(xiě)法:Java:單例模式我只推薦兩種。還需掌握volatilesynchronized的知識(shí)點(diǎn)。

由于個(gè)人能力有限,有錯(cuò)誤歡迎指正;或者覺(jué)得方向不好,歡迎指導(dǎo),非常感謝。

最后希望點(diǎn)贊&關(guān)注=true

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

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