天天看點

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

有部分同學回報說<code>Volatile</code>修飾的共享變量不具有原子性,從程式角度去了解,<code>volatile</code>變量确實不具有原子性,而是在可見性。

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

最典型的例子,就是64位的<code>long</code>和<code>double</code>類型變量,可能被拆成32位的兩次讀寫,如果進行并發,執行效果不一定符合預期。盡管JDK5開始後的JSR-133記憶體模型增強了,隻允許64位的<code>long</code>和<code>double</code>類型變量的寫可以分兩次32位寫,讀隻能一次性到位,具有原子性。而通過<code>volatile</code>修飾的變量,其讀寫都是具有原子性。

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

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

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

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

通過面試10問,讓大家掌握單例模式的雙重檢查模式和靜态内部類單例模式,并了解其中原理。從原理進而引出本文的重點:<code>volatile</code>和<code>synchronized</code>。

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

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

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

雙重檢查模式:

靜态内部類模式:

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

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

構造函數得私有,禁止其他對象直接建立執行個體;

對外提供一個靜态方法,可以擷取唯一的執行個體;

即然是雙重檢查模式,就意味着建立執行個體過程會有兩層檢查。第一層就是最外層的判空語句:<code>代碼3.1處的if (singleton == null)</code>,該判斷沒有加鎖處理,避免第一次檢查<code>singleton</code>對象非<code>null</code>時,多線程加鎖和初始化操作;目前對象未建立時,通過<code>synchronized</code>關鍵字同步代碼塊,持有目前<code>Singleton.class</code>的鎖,保證線程安全,然後進行第二次檢查。

<code>Singleton</code>類持有的<code>singleton</code>執行個體引用需要<code>volatile</code>關鍵字修飾,因為在最後一步<code>singleton = new Singleton();</code> 建立執行個體的時候可能會重排序,導緻<code>singleton</code>對象逸出,導緻其他線程擷取到一個未初始化完畢的對象。

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

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

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

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

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

1、将目前處理器緩存行的資料寫回到系統記憶體;

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

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

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

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

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

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

第一個操作是對volatile變量的寫,第二個操作是對volatile的讀,禁止重排序;

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

從第2條規則就可以了解通過添加<code>volatile</code>關鍵字修飾單例的引用,可以禁止重排序。

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

volatile寫操作前插入StoreStore屏障;

volatile寫操作後插入StoreLoad屏障;

volatile讀操作後插入LoadLoad屏障;

volatile讀操作後插入LoadStore屏障;

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

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

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

程式順序規則:一個線程中的每個操作,happens-before于該線程的任意後續操作;

螢幕原則:對一個鎖的解鎖,happens-before于随後對該鎖的加鎖;

volatile規則:對一個volatile變量的寫,happens-before 于任意後續對這個volatile域的讀;

傳遞性:如果A happes-before B,B happens-before C,那麼A happens-before C;

start()原則:線程A執行ThreadB.start()操作,start() happens-before 線程B内所有操作;

jion()原則:如果線程A執行 ThreadB.jion()并成功傳回,那線程B的所有操作都happens-before 于A從jion()操作成功傳回。

答:在<code>代碼3.2處</code>,用到了<code>synchronized</code> 關鍵字,對<code>Singletion.Class</code>對象進行了同步,確定了在多線程環境下隻有一個線程對<code>Singletion</code>類的Class對象進行執行個體化。在Java中,每一個對象都可以作為鎖:

對于普通同步方法,鎖是目前執行個體對象;

對于靜态同步方法,鎖是目前類的Class對象;

對于同步方法塊,鎖是Synchoized括号的Class對象。

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

當一個線程A擷取到這個初始化鎖時,其他線程想要擷取初始化鎖隻能等待;線程A執行類靜态初始化和初始化靜态字段的過程,就算發生類似雙重檢查模式的重排序,對結果也沒有影響,因為此時沒有其他線程可以捕獲到初始化鎖。線程A初始化完畢,釋放鎖并通知等待擷取初始化鎖的線程。根據<code>happens-befroe</code>關系中的螢幕規則,當其他線程擷取到初始鎖時,已經能看到線程A的初始化所有操作,此時靜态對象已經初始化完畢,其他線程無需再初始化。

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

<code>Synchronized</code>用的鎖是存放在Java的對象頭;如果對象是數組,用3字寬存儲對象頭,其中一字寬用于存儲數組長度;非數組,則2字寬存儲對象頭。在32位虛拟機,1字寬=4位元組=32位。

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

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

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

偏向鎖: 線程在大多數情況下并不存在競争條件,使用同步會消耗性能,而偏向鎖是對鎖的優化,可以消除同步,提升性能。當一個線程獲得鎖,會将對象頭的鎖标志位設為01,進入偏向模式.偏向鎖可以在讓一個線程一直持有鎖,在其他線程需要競争鎖的時候,再釋放鎖。==》隻有一個線程進入臨界區。

輕量級鎖: 當線程A獲得偏向鎖後,線程B進入競争狀态,需要獲得線程A持有的鎖,那麼線程A撤銷偏向鎖,進入無鎖狀态。線程A和線程B交替進入臨界區,偏向鎖無法滿足,膨脹到輕量級鎖,鎖标志位設為00。==》多個線程交替進入臨界區。

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

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

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

通過本文,需要掌握雙重檢查模式和靜态内部類模式這<code>單例模式</code>的兩種寫法,還需掌握<code>volatile</code>和<code>synchronized</code>的知識點。

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

作者:新小夢

連結:https://juejin.cn/post/6856964867811721229

來源:掘金

著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。