全面解讀volatile和synchronize,輕鬆掌握Volatile與Synchronized

南方吳彥祖_藍斯發表於2021-09-28

有部分同學反饋說 Volatile修飾的共享變數不具有原子性,從程式角度去理解, volatile變數確實不具有原子性,而是在可見性。

而文中,我也特意強調是對 單個 volatile變數讀寫具有原子性,這是從記憶體語義角度出發的。對單個 volatile變數的讀寫與一個普通變數的讀寫操作都是使用一個鎖來同步,他麼之間的執行效果是相同的。

最典型的例子,就是64位的 longdouble型別變數,可能被拆成32位的兩次讀寫,如果進行併發,執行效果不一定符合預期。儘管JDK5開始後的JSR-133記憶體模型增強了,只允許64位的 longdouble型別變數的寫可以分兩次32位寫,讀只能一次性到位,具有原子性。而透過 volatile修飾的變數,其讀寫都是具有原子性。

對於這種 i++,是屬於複合操作,就是其他同學所說不具有原子性的出發點了。

前言

Android開發者來說,相信對併發程式設計知識的掌握是非常薄弱的,一直是個人進階的軟肋之一。對於併發實踐經驗缺乏的開發者來說,文縐縐的技術書籍和部落格,會比較羞澀難懂。從本文開始,嘗試著逐個攻破併發程式設計的基礎知識點。

由於無知與惰性,讓我們感覺摸到了技術的天花板!

面試10問

本文結合個人實際面試經驗和最近學習歸納總結而出,歡迎各位大佬 點贊支援。

透過面試10問,讓大家掌握 單例模式雙重檢查模式靜態內部類單例模式,並瞭解其中原理。從原理進而引出本文的重點: volatilesynchronized

第1問:平常在Android開發中,有用到哪麼設計模式麼?

當時回答:平常用的比較多的是單例模式、構造者模式、工廠模式。尤其是單例模式中雙重檢查模式和靜態類單例模式;能夠保證多執行緒物件唯一,不會建立多個例項導致程式執行錯誤或影響效能。

解讀:雖然設計模式有很多種,個人來說,經常用也就單例模式了。雖然面試前突擊瀏覽複習了,然面試一緊張,沒啥卵用。所以回答一定要往自己瞭解的說,並引導面試官往自己會的問。

第2問:在紙上寫一下雙重檢查模式和靜態類單例模式程式碼?

心理活動:還好面試前自己已經默寫過很多遍了,問題不大,嘩啦啦的寫出來:

雙重檢查模式:

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;  
    }  
}
複製程式碼

靜態內部類模式:

public class Singleton { 
    private Singleton(){
    }      public static Singleton getSingleton(){  
        return Inner.instance;  
    }  
    private static class Inner {  
        private static final Singleton instance = new Singleton();  
    }  
} 
複製程式碼

寫好遞給面試官:雙重檢查模式和單例模式都能夠有效保證執行緒安全,又都是延時初始化,能夠減少不必要的效能開銷。

第3問:雙重檢查模式有什麼需要注意地方?

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;
    }
}
複製程式碼

答:雙重檢查模式需要注意以下幾點:

  1. 建構函式得私有,禁止其他物件直接建立例項;
  2. 對外提供一個靜態方法,可以獲取唯一的例項;
  3. 即然是雙重檢查模式,就意味著建立例項過程會有兩層檢查。第一層就是最外層的判空語句: 程式碼3.1處的if (singleton == null),該判斷沒有加鎖處理,避免第一次檢查 singleton物件非 null時,多執行緒加鎖和初始化操作;當前物件未建立時,透過 synchronized關鍵字同步程式碼塊,持有當前 Singleton.class的鎖,保證執行緒安全,然後進行第二次檢查。
  4. Singleton類持有的 singleton例項引用需要 volatile關鍵字修飾,因為在最後一步 singleton = new Singleton(); 建立例項的時候可能會重排序,導致 singleton物件逸出,導致其他執行緒獲取到一個未初始化完畢的物件。

第4問:剛剛講到的第四點,為什麼會有重排序, volatile關鍵字如何禁止重排序?

答:重排序是指編輯器和處理器為了最佳化程式效能而對指令序列進行重排序的一種手段。只要遵守 as -if-serial語義(無論怎麼重排序,單執行緒程式的執行結果不會改變)。所以編譯器為了最佳化效能,可能會對下圖中2和3步驟進行重排序,這種重排序時允許的,因為不會改變單執行緒(目前只有該執行緒獨佔該程式碼塊)內程式的執行結果。

全面解讀volatile和synchronize,輕鬆掌握Volatile與Synchronized

在單執行緒環境是沒有問題,如果在多執行緒環境下,程式的執行結果就會被破壞。如下圖所示,執行緒B在第一步判空時,singleton例項的引用已經非null,所以它不進入申請鎖階段,而直接訪問物件,但此物件還沒初始化完成,那麼物件在實際使用就會出各種問題。


全面解讀volatile和synchronize,輕鬆掌握Volatile與Synchronized

volatile修飾的變數本身具有可見性和原子性,所謂的可見性是指對一個volatile變數的讀值,讀到的值是所有執行緒中最新修改的值;而原子性是指對 單個變數的讀寫具有原子性。之所以會有這兩個特性,是因為會在該共享變數的彙編指令之前增加 Lock指令,該 Lock字首指令會在多核處理器做兩件事:

1、將當前處理器快取行的資料寫回到系統記憶體;

2、這個寫回記憶體的操作會使其他處理器裡快取了該記憶體地址的資料無效。

ps:單核處理器一時刻只能有一條執行緒執行,多執行緒是指單核CPU對不同執行緒進行上下文切換和排程;多核處理器同一個時刻可能多條執行緒(每個核一條執行緒)併發執行, 這時同步非常重要,現代CPU基本都是多核了。

由於volatie變數的可見性這個特性使其  寫-讀 建立起了 happens-before關係,從記憶體語義的角度上說,執行緒A寫一個 volatile變數,實質上是執行緒A向接下來將要讀這個 volatiel變數的某個執行緒發出了通知。原理上講的話,在寫一個 volatile變數是,JAVA記憶體模型(JMM)會把該執行緒對應的本地記憶體中的共享變數重新整理到主記憶體;而在讀 volatile變數時,會把該執行緒對應的本地記憶體置為無效,從主記憶體中讀取該變數。執行緒之間透過共享程式的 volatile變數(共享狀態),透過寫讀操作共享狀態進行隱式通訊。

JMM為了實現這種 volatile記憶體語義,會限制編譯器和處理器的部分重排序。

為編譯器最佳化制定以下 三條規則 :

  • 第一個操作是對volatile變數的讀,無論第二個操作是什麼,都禁止重排序;
  • 第一個操作是對volatile變數的寫,第二個操作是對volatile的讀,禁止重排序;
  • 第二個操作是對volatile變數的寫,無論第一個操作是什麼,都禁止重排序;

從第2條規則就可以理解透過新增 volatile關鍵字修飾單例的引用,可以禁止重排序。

根據這三條規則,編譯器會在生成位元組碼時,在指令序列插入適當的,保守策略的記憶體屏障(一組CPU指令,實現對記憶體操作的順序限制)。

  • volatile寫操作前插入StoreStore屏障;
  • volatile寫操作後插入StoreLoad屏障;
  • volatile讀操作後插入LoadLoad屏障;
  • volatile讀操作後插入LoadStore屏障;

以上記憶體屏障時非常保守,編譯器在生成位元組碼時,也會進行部分最佳化,減少一些不必要的記憶體屏障,以提高效能。不同的處理器會根據自身的記憶體模型繼續最佳化。

ps:JMM是為了遮蔽底層硬體記憶體模型不一致,為頂層開發提供一套標準的記憶體模型,讓開發這專注要業務開發。

第5問:剛剛提到的happens-before規則,具體怎麼說來的?

答:從JDK5開始,使用了新的JSR-133記憶體模型,該模型定義了 happens-before 規則:

  1. 程式順序規則:一個執行緒中的每個操作,happens-before於該執行緒的任意後續操作;
  2. 監視器原則:對一個鎖的解鎖,happens-before於隨後對該鎖的加鎖;
  3. volatile規則:對一個volatile變數的寫,happens-before 於任意後續對這個volatile域的讀;
  4. 傳遞性:如果A happes-before B,B happens-before C,那麼A happens-before C;
  5. start()原則:執行緒A執行ThreadB.start()操作,start() happens-before 執行緒B內所有操作;
  6. jion()原則:如果執行緒A執行 ThreadB.jion()併成功返回,那執行緒B的所有操作都happens-before 於A從jion()操作成功返回。

第6問:規則第2點講到了鎖,那鎖在雙重檢查單例模式起了什麼作用?

答:在 程式碼3.2處,用到了 synchronized 關鍵字,對 Singletion.Class物件進行了同步,確保了在多執行緒環境下只有一個執行緒對 Singletion類的Class物件進行例項化。在Java中,每一個物件都可以作為鎖:

  1. 對於普通同步方法,鎖是當前例項物件;
  2. 對於靜態同步方法,鎖是當前類的Class物件;
  3. 對於同步方法塊,鎖是Synchoized括號的Class物件。

第7問:靜態內部類單例模式有沒有用到鎖?

答:有的,JVM在類的初始化階段(在Class被載入後,且線上程使用之前),會執行類的初始化,JVM會去獲取一個鎖,這個鎖能同步多個執行緒對同一個類的初始化。

當一個執行緒A獲取到這個初始化鎖時,其他執行緒想要獲取初始化鎖只能等待;執行緒A執行類靜態初始化和初始化靜態欄位的過程,就算發生類似雙重檢查模式的重排序,對結果也沒有影響,因為此時沒有其他執行緒可以捕獲到初始化鎖。執行緒A初始化完畢,釋放鎖並通知等待獲取初始化鎖的執行緒。根據 happens-befroe關係中的監視器規則,當其他執行緒獲取到初始鎖時,已經能看到執行緒A的初始化所有操作,此時靜態物件已經初始化完畢,其他執行緒無需再初始化。

第8問:瞭解過鎖的原理,知道鎖儲存在哪麼?

答:JVM(Java虛擬機器)是基於進入和退出 Monitor物件來實現方法同步和程式碼塊同步的。同步程式碼塊使用 monitorenter指令在編譯後插入到同步程式碼塊的開始位置,使用 monitorexit插入到同步程式碼塊的結束處或異常處, monitorenter必須有對應 monitorexit指令與之配對。任何物件都有一個 monitor與之相關聯,當且一個 monitor被持有後,將處於鎖定狀態。執行緒執行到 monitorenter指令時,將會嘗試獲取物件所對應的 monitor的所有權,即獲得物件的鎖。方法則是在方法的指令前增加 ACC_SYNCHRONIZED修飾符。

Synchronized用的鎖是存放在Java的物件頭;如果物件是陣列,用3字寬儲存物件頭,其中一字寬用於儲存陣列長度;非陣列,則2字寬儲存物件頭。在32位虛擬機器,1字寬=4位元組=32位。

全面解讀volatile和synchronize,輕鬆掌握Volatile與Synchronized

第9問:即然瞭解過Java的物件頭,那應該清楚鎖升級的幾種狀態吧,說一下?

答:在Java SE6,為了減少獲得鎖和釋放鎖帶來的效能消耗,引入了 偏向鎖輕量級鎖。意味著此時鎖從低到高共有四種狀態:無鎖狀態、偏向鎖狀態、輕量級鎖狀態和重量級鎖狀態。鎖的狀態是根據執行緒對鎖的競爭情況來定義的。32位JVM執行狀態下,Mark Work的儲存結構:

全面解讀volatile和synchronize,輕鬆掌握Volatile與Synchronized

偏向鎖: 執行緒在大多數情況下並不存在競爭條件,使用同步會消耗效能,而偏向鎖是對鎖的最佳化,可以消除同步,提升效能。當一個執行緒獲得鎖,會將物件頭的鎖標誌位設為01,進入偏向模式.偏向鎖可以在讓一個執行緒一直持有鎖,在其他執行緒需要競爭鎖的時候,再釋放鎖。==》只有一個執行緒進入臨界區。

輕量級鎖: 當執行緒A獲得偏向鎖後,執行緒B進入競爭狀態,需要獲得執行緒A持有的鎖,那麼執行緒A撤銷偏向鎖,進入無鎖狀態。執行緒A和執行緒B交替進入臨界區,偏向鎖無法滿足,膨脹到輕量級鎖,鎖標誌位設為00。==》多個執行緒交替進入臨界區。

重量級鎖: 當多執行緒交替進入臨界區,輕量級鎖hold得住。但如果多個執行緒同時進入臨界區,hold不住了,膨脹到重量級鎖==》多個執行緒同時進入臨界區。

第10問:為什麼Synchronized夠用,還要增加Volatile?

Volatile相對 Synchronized來說在同步上比較輕量級,能夠有效降低CPU頻繁的執行緒上下文切換和排程。同時, Volatile的原子操作是針對單個 volatile變數的寫讀操作,無法和 Sychronized對整個方法或程式碼塊起的作用相比較。

總結

基本每一問都會涉及到一些知識點,面試官也會從不同方向去提問,引出不同知識點。例如後面幾個問題可以引出Java的記憶體模型,這些都是面試的高頻問題。

透過本文,需要掌握雙重檢查模式和靜態內部類模式這 單例模式的兩種寫法,還需掌握 volatilesynchronized的知識點。

更多Android技術分享可以關注@我,也可以加入QQ群號:1078469822,學習交流Android開發技能。

作者:新小夢
連結:
著作權歸作者所有。商業轉載請聯絡作者獲得授權,非商業轉載請註明出處。

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69983917/viewspace-2794242/,如需轉載,請註明出處,否則將追究法律責任。

相關文章