Java 併發之內存模型,看這篇就對了

點擊上方 "程序員小樂"關注公眾號, 星標或置頂一起成長

每天凌晨00點00分, 第一時間與你相約

每日英文

No matter how hard life may get sometimes, if we stay strong, we will always conquer.

無論日子多艱難,只要我們保持一顆堅強的心,一切都會過去的。

每日掏心話

敢於挑戰逆境的人,生命因此茁壯。要感謝給你提意見的人,他使你成熟;要感謝給你造困境的人,他使你堅強。

來自:killianxu | 責編:樂樂

鏈接:cnblogs.com/killianxu/p/11665903.html

Java 併發之內存模型,看這篇就對了

程序員小樂(ID:study_tech)第 672 次推文 圖片來自網絡

往日回顧:衝冠一怒為代碼:論程序員與負能量

正文

Java 併發之內存模型,看這篇就對了

java內存模型知識導圖

1、併發問題及含義

併發編程存在原子性、可見性、有序性問題。

  • 原子性即一系列操作要麼都執行,要麼都不執行。

  • 可見性,一個線程對共享變量的修改,另一個線程可能不會馬上看到。由於多核CPU,每個CPU核都有高速緩存,會緩存共享變量,某個線程對共享變量的修改會改變高速緩存中的值,但卻不會馬上寫入內存。另一個線程讀到的是另一個核緩存的共享變量的值,出現緩存不一致問題。

  • 有序性,即程序執行的順序按照代碼的先後順序執行。編譯器和處理器會對指令進行重排,以優化指令執行性能,重排不會改變單線程執行結果,但在多線程中可能會引起各種各樣的問題。

2、內存模型

為了保證共享內存的正確性(可見性、有序性、原子性),內存模型定義了共享內存系統中多線程程序讀寫操作行為的規範。內存模型解決併發問題

主要採用兩種方式:限制處理器優化和使用內存屏障。

順序一致性內存模型是一種理論參考模型,提供了極強的內存可見性保證,具有兩大特性:

  • 一個線程的所有操作按照程序的順序執行,而不能重排序。

  • 所有線程只能看到單一的執行順序。每個操作都必須原子執行且立刻對其它線程可見。

順序一致性內存模型禁止很多處理器和編譯器重排,影響執行性能,處理器內存模型和JMM對順序一致性內存模型進行放鬆,執行性能:處理器內存模型>JMM>順序一致性內存模型,易編程性:處理器內存模型

3、java內存模型

Java內存模型(Java Memory Model ,JMM)是一種符合內存模型規範的,屏蔽了各種硬件和操作系統的訪問差異的,保證了Java程序在各種平臺下對內存的訪問都能保證效果一致的機制及規範。

Java內存模型規定了所有的變量都存儲在主內存中,每條線程還有自己的工作內存,線程的工作內存中保存了該線程中是用到的變量的主內存副本拷貝,線程對變量的所有操作都必須在工作內存中進行,而不能直接讀寫主內存。不同的線程之間也無法直接訪問對方工作內存中的變量,線程間變量的傳遞均需要自己的工作內存和主存之間進行數據同步進行。主內存和工作內存可類比成計算機內存模型中的主存和緩存的概念。

3.1 java內存模型解決併發問題方法

原子性,在java中,只有簡單的讀取、賦值(而且必須是將數字賦值給某個變量,變量之間的相互賦值不是原子操作)才是原子操作。在32位平臺下,對64位數據的賦值是需要通過兩個操作來完成,不能保證其原子性。要實現更大範圍操作的原子性,可以通過synchronized和Lock來實現。由於synchronized和Lock保證任一時刻只有一個線程執行該代碼塊,從而保證了原子性。

可見性,Java提供了volatile關鍵字來保證可見性,當一個共享變量被volatile修飾時,它會保證修改的值會立即被更新到主存,當有其他線程需要讀取時,它會去內存中讀取新值。通過synchronized和Lock也能夠保證可見性,synchronized和Lock能保證同一時刻只有一個線程獲取鎖然後執行同步代碼,並且在釋放鎖之前會將對變量的修改刷新到主存當中。因此可以保證可見性。

JMM通過happens-before關係向程序員提供跨線程的內存可見性保證:

  • 程序次序規則:一段代碼在單線程中執行的結果是有序的。注意是執行結果,因為虛擬機、處理器會對指令進行重排序(重排序後面會詳細介紹)。雖然重排序了,但是並不會影響程序的執行結果,所以程序最終執行的結果與順序執行的結果是一致的。故而這個規則只對單線程有效,在多線程環境下無法保證正確性。

  • 鎖定規則:這個規則比較好理解,無論是在單線程環境還是多線程環境,一個鎖處於被鎖定狀態,那麼必須先執行unlock操作後面才能進行lock操作。

  • volatile變量規則:這是一條比較重要的規則,它標誌著volatile保證了線程可見性。通俗點講就是如果一個線程先去寫一個volatile變量,然後一個線程去讀這個變量,那麼這個寫操作一定是happens-before讀操作的。

  • 傳遞規則:提現了happens-before原則具有傳遞性,即A happens-before B , B happens-before C,那麼A happens-before C

  • 線程啟動規則:假定線程A在執行過程中,通過執行ThreadB.start()來啟動線程B,那麼線程A對共享變量的修改在接下來線程B開始執行後確保對線程B可見。

  • 線程終結規則:假定線程A在執行的過程中,通過制定ThreadB.join()等待線程B終止,那麼線程B在終止之前對共享變量的修改在線程A等待返回後可見。

有序性,可以使用synchronized和volatile來保證多線程之間操作的有序性。實現方式有所區別:volatile關鍵字會禁止指令重排。synchronized關鍵字保證同一時刻只允許一條線程操作。

3.2 java併發原語

Java內存模型,除了定義了一套規範,還提供了一系列原語,封裝了底層實現後,供開發者直接使用。

3.2.1 volatile

內存語義:

當寫一個volatile變量時,JMM會把該線程對應的本地內存中的所有共享變量刷新到主內存。

當讀一個volatile變量,JMM會把該線程對應的本地內存置為無效,線程接下來從主內存中讀取共享變量。

實現:

編譯器在生成字節碼時,會在指令序列中插入內存屏障來禁止特定類型的處理器重排序。

在每個volatile寫操作前面插入一個StoreStore屏障。StoreStore屏障禁止上面的普通寫和volatile寫重排序,保障上面的普通寫在volatile寫之前刷新到主內存。

在每個volatile寫操作後面插入一個StoreLoad屏障。避免volatile寫與後面可能有的volatile讀/寫重排序。

在每個volatile讀操作的後面插入一個LoadLoad屏障。禁止下面的普通讀操作和上面的volatile讀操作重排序

在每個volatile讀操作的後面插入一個LoadStore屏障。禁止下面的普通寫操作和上面的volatile讀操作重排序

3.2.2 synchronized

內存語義:

當線程釋放鎖時,JMM會把該線程對應的本地內存中的共享變量刷新到主內存中.

當線程獲取鎖時,JMM會把該線程對應的本地內存置為無效。從而使得被監視器保護的臨界區代碼必須從主內存中讀取共享變量.

實現:

java對象頭組成:

  • Mark Word

  • 指向類的指針

  • 數組長度(只有數組對象才有)

Mark Word用於加鎖操作,結構如下:

Java 併發之內存模型,看這篇就對了

圖3.1 java對象頭Mark Word

synchronized用的鎖是存在Java對象頭裡,任何java對象都存在一個鎖,JVM基於進入和退出Monitor對象來實現方法同步和代碼塊同步。代碼塊同步是使用monitorenter和monitorexit指令實現的,monitorenter指令是在編譯後插入到同步代碼塊的開始位置,而monitorexit是插入到方法結束處和異常處。

監視器鎖(Monitor)本質依賴操作系統的Mutex Lock(互斥鎖)來實現,如果互斥量已經上鎖,調用線程會阻塞,阻塞或喚醒一條線程,都需要操作系統來幫忙完成,這就需要從用戶態轉換到核心態中,因此狀態轉換需要耗費很多的處理器時間。在jdk1.6中加入對鎖的優化措施,鎖一共有4種狀態,級別從低到高依次是:無鎖狀態、偏向鎖狀態、輕量級鎖狀態和重量級鎖狀態。鎖可以升級但不能降級。

偏向鎖:

當一個線程訪問同步塊並獲取鎖時,會在對象頭和棧幀中的鎖記錄裡存儲鎖偏向的線程ID,以後該線程在進入和退出同步塊時不需要進行CAS操作來加鎖和解鎖,只需簡單地測試一下對象頭的Mark Word裡是否存儲著指向當前線程的偏向鎖。引入偏向鎖是為了在無多線程競爭的情況下儘量減少不必要的輕量級鎖執行路徑,因為輕量級鎖的獲取及釋放依賴多次CAS原子指令,而偏向鎖只需要在置換ThreadID的時候依賴一次CAS原子指令(由於一旦出現多線程競爭的情況就必須撤銷偏向鎖,所以偏向鎖的撤銷操作的性能損耗必須小於節省下來的CAS原子指令的性能消耗)。

輕量級鎖:

輕量級鎖是為了在線程近乎交替執行同步塊時提高性能。多個線程競爭鎖,若當前只有一個等待線程,則可通過自旋稍微等待一下,可能另一個線程很快就會釋放鎖。 但是當自旋超過一定的次數,或者一個線程在持有鎖,一個在自旋,又有第三個來訪時,輕量級鎖膨脹為重量級鎖。

重量級鎖:

重量級鎖是通過對象內部的一個叫做監視器鎖(monitor)來實現的,監視器鎖本質又是依賴於底層的操作系統的Mutex Lock(互斥鎖)來實現的。而操作系統實現線程之間的切換需要從用戶態轉換到核心態,這個成本非常高。

其它鎖優化措施:鎖消除、鎖粗化、自旋鎖(忙循環,適用持有鎖的線程很快釋放鎖)、自適應的自旋鎖(自旋次數不固定,前一次在同一個鎖上的自旋時間及鎖的擁有者的狀態決定)。

3.2.3 final

寫final域禁止把final域的寫重排序到構造函數之外。對於引用類型:在構造函數內對final域引用對象的成員域的寫入,與在構造函數外將這個被構造對象的引用賦值給引用變量,這兩個操作不能重排序。防止對象構造完成,未被初始化的final域被訪問(要達到此目的,還需確保被構造對象不能在構造函數中“逸出”)

讀final域禁止初次讀一個對象的引用和隨後初次讀這個對象包含的final域之間的重排序。確保在讀一個對象的final域前,一定會先讀包含這個final域對象的引用,如果引用不為空,引用對象的final域已經被初始化過。

實現:

JMM禁止編譯器把final域的寫重排序到構造函數之外。

編譯器在final域的寫之後,構造函數return之前,插入StoreStore屏障,禁止處理器把final域的寫重排序到構造函數之外。

編譯器會在讀final域前面插入StoreStore屏障。

歡迎在留言區留下你的觀點,一起討論提高。如果今天的文章讓你有新的啟發,學習能力的提升上有新的認識,歡迎轉發分享給更多人。

歡迎各位讀者加入程序員小樂技術群,在公眾號後臺回覆“加群”或者“學習”即可。

猜你還想看

阿里、騰訊、百度、華為、京東最新面試題彙集

Spring MVC 啟動源碼解析,看這篇就對了!

初級開發者應該從Spring源碼中學什麼?

Java RESTful 框架的性能比較

關注微信公眾號「程序員小樂」,收看更多精彩內容

嘿,你在看嗎?


分享到:


相關文章: