《深刻理解Java內存模型》讀書總結

 

概要

文章是《深刻理解Java內容模型》讀書筆記,該書總共包括了3部分的知識。html

1部分,基本概念
java

                   包括「併發、同步、主內存、本地內存、重排序、內存屏障、happens before規則、as-if-serial規則、數據依賴性、順序一致性模型、JMM的含義和意義」。程序員

2部分,同步機制編程

                  該部分中就介紹了「同步」的3種方式:volatile、鎖、final。對於每一種方式,從該方式的「特性」、「創建的happens before關係」、「對應的內存語義」、「實現方式」等幾個方面進行了分析說明。實際上,JMM保證「若是程序正確同步,則執行結果與順序一致性內存模型的結果相同」的機制;而這部分這是確保程序正確同步的機制。數組

3部分,JMM總結多線程

 

第1部分 基本概念

1. 併發併發

定義:即,併發(同時)發生。操做系統中,是指一個時間段中有幾個程序都處於已啓動運行到運行完畢之間,且這幾個程序都是在同一個處理機上運行,但任一個時刻點上只有一個程序在處理機上運行。app

併發須要處理兩個關鍵問題:線程之間如何通訊線程之間如何同步。函數

(01) 通訊 —— 是指線程之間如何交換信息。在命令式編程中,線程之間的通訊機制有兩種:共享內存和消息傳遞。性能

(02) 同步—— 是指程序用於控制不一樣線程之間操做發生相對順序的機制。在Java中,能夠經過volatilesynchronized, 鎖等方式實現同步。


2.主內存和本地內存

主內存     —— 即main memory。在java中,實例域、靜態域和數組元素是線程之間共享的數據,它們存儲在主內存中。

本地內存 —— 即local memory。 局部變量,方法定義參數 和 異常處理器參數是不會在線程之間共享的,它們存儲在線程的本地內存中。


3.重排序

定義:重排序是指「編譯器和處理器」爲了提升性能,而在程序執行時會對程序進行的重排序。

說明:重排序分爲——「編譯器」和「處理器」兩個方面,而「處理器」重排序又包括「指令級重排序」和「內存的重排序」。

關於重排序,咱們須要理解它的思想:爲了提升程序的併發度,從而提升性能!可是對於多線程程序,重排序可能會致使程序執行的結果不是咱們須要的結果!所以,就須要咱們經過「volatilesynchronize,鎖等方式」做出正確的實現同步。


4.內存屏障

定義:包括LoadLoad, LoadStore, StoreLoad, StoreStore4種內存屏障。內存屏障是與相應的內存重排序相對應的。

屏障類型

指令示例

說明

LoadLoad Barriers

Load1; LoadLoad; Load2

確保Load1數據的裝載,以前於Load2及全部後續裝載指令的裝載。

StoreStore Barriers

Store1; StoreStore; Store2

確保Store1數據對其餘處理器可見(刷新到內存),以前於Store2及全部後續存儲指令的存儲。

LoadStore Barriers

Load1; LoadStore; Store2

確保Load1數據裝載,以前於Store2及全部後續的存儲指令刷新到內存。

StoreLoad Barriers

Store1; StoreLoad; Load2

確保Store1數據對其餘處理器變得可見(指刷新到內存),以前於Load2及全部後續裝載指令的裝載。StoreLoad Barriers會使該屏障以前的全部內存訪問指令(存儲和裝載指令)完成以後,才執行該屏障以後的內存訪問指令。

做用:經過內存屏障能夠禁止特定類型處理器的重排序,從而讓程序按咱們預想的流程去執行。

 

5. happens-before

定義JDK5(JSR-133)提供的概念,用於描述多線程操做之間的內存可見性。若是一個操做執行的結果須要對另外一個操做可見,那麼這兩個操做之間必須存在happens-before關係。

做用:描述多線程操做之間的內存可見性。

         [程序順序規則]:一個線程中的每一個操做,happens- before 於該線程中的任意後續操做。

         [監視器鎖規則]:對一個監視器鎖的解鎖,happens- before 於隨後對這個監視器鎖的加鎖。

         [volatile變量規則]:對一個volatile域的寫,happens- before 於任意後續對這個volatile域的讀。

         [傳遞性]:若是A happens- before B,且B happens- before C,那麼A happens- before C

 

6. 數據依賴性

定義:若是兩個操做訪問同一個變量,且這兩個操做中有一個爲寫操做,此時這兩個操做之間就存在數據依賴性。

做用:編譯器和處理器不會對「存在數據依賴關係的兩個操做」執行重排序。


7.as-if-serial

定義:無論怎麼重排序,程序的執行結果不能被改變。

 

8. 順序一致性內存模型

定義:它是理想化的內存模型。有如下規則:

        (01) 一個線程中的全部操做必須按照程序的順序來執行。

        (02) 全部線程都只能看到一個單一的操做執行順序。在順序一致性內存模型中,每一個操做都必須原子執行且馬上對全部線程可見。

 

9. JMM

定義Java Memory Mode,即Java內存模型。它是Java線程之間通訊的控制機制

說明JMMJava程序做出保證——若是程序是正確同步的,程序的執行將具備順序一致性。即,程序的執行結果與該程序在順序一致性內存模型中的執行結果相同。


10. 可見性

可見性通常用於指不一樣線程之間的數據是否可見。

java中, 實例域、靜態域和數組元素這些數據是線程之間共享的數據,它們存儲在主內存中;主內存中的全部數據對該內存中的線程都是可見的。而局部變量,方法定義參數 和 異常處理器參數這些數據是不會在線程之間共享的,它們存儲在線程的本地內存中;它們對其它線程是不可見的。

此外,對於主內存中的數據,在本地內存中會對應的建立該數據的副本(至關於緩衝);這些副本對於其它線程也是不可見的。


11. 原子性

是指一個操做是按原子的方式執行的。要麼該操做不被執行;要麼以原子方式執行,即執行過程當中不會被其它線程中斷。


第2部分 同步機制

1.volatile

1.1 做用

若是一個變量是volatile類型,則對該變量的讀寫就將具備原子性。若是是多個volatile操做或相似於volatile++這種複合操做,這些操做總體上不具備原子性。volatile變量自身具備下列特性:

      [可見性]:對一個volatile變量的讀,老是能看到(任意線程)對這個volatile變量最後的寫入。

      [原子性]:對任意單個volatile變量的讀/寫具備原子性,但相似於volatile++這種複合操做不具備原子性。

 

1.2 volatile的內存語義

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

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

1.3 JMM中的實現方式

JMM針對編譯器制定的volatile重排序規則表:

是否能重排序

第二個操做

第一個操做

普通讀/

volatile

volatile

普通讀/

 

 

NO

volatile

NO

NO

NO

volatile

 

NO

NO


下面是基於保守策略的JMM內存屏障插入策略:

在每一個volatile寫操做的前面插入一個StoreStore屏障。
在每一個volatile寫操做的後面插入一個StoreLoad屏障。
在每一個volatile讀操做的後面插入一個LoadLoad屏障。
在每一個volatile讀操做的後面插入一個LoadStore屏障。

  

1.4 volatilesynchronize對比

在功能上,監視器鎖比volatile更強大;在可伸縮性和執行性能上,volatile更有優點。

volatile僅僅保證對單個volatile變量的讀/寫具備原子性;而synchronize鎖的互斥執行的特性能夠確保對整個臨界區代碼的執行具備原子性。



2.

2.1 做用

鎖是java併發編程中最重要的同步機制。

2.2 鎖的內存語義

     (01) 線程A釋放一個鎖,實質上是線程A向接下來將要獲取這個鎖的某個線程發出了(線程A對共享變量所作修改的)消息。

     (02) 線程B獲取一個鎖,實質上是線程B接收了以前某個線程發出的(在釋放這個鎖以前對共享變量所作修改的)消息。

     (03) 線程A釋放鎖,隨後線程B獲取這個鎖,這個過程實質上是線程A經過主內存向線程B發送消息。

2.3 JMM如何實現鎖

公平鎖

公平鎖是經過「volatile」實現同步的。公平鎖在釋放鎖的最後寫volatile變量state;在獲取鎖時首先讀這個volatile變量。根據volatilehappens-before規則,釋放鎖的線程在寫volatile變量以前可見的共享變量,在獲取鎖的線程讀取同一個volatile變量後將當即變的對獲取鎖的線程可見。

非公平鎖

經過CAS實現的,CAS就是compare and swapCAS實際上調用的JNI函數,也就是CAS依賴於本地實現。以Intel來講,對於CASJNI實現函數,它保證:(01)禁止該CAS以前和以後的讀和寫指令重排序。(02)把寫緩衝區中的全部數據刷新到內存中。



3.final

3.1 特性

對於基本類型final域,編譯器和處理器要遵照兩個重排序規則:

(01) final寫:「構造函數內對一個final域的寫入」,與「隨後把這個被構造對象的引用賦值給一個引用變量」,這兩個操做之間不能重排序。

(02) final讀:「初次讀一個包含final域的對象的引用」,與「隨後初次讀對象的final域」,這兩個操做之間不能重排序。

對於引用類型final域,除上面兩條以外,還有一條規則:

(03) final寫:在「構造函數內對一個final引用的對象的成員域的寫入」,與「隨後在構造函數外把這個被構造對象的引用賦值給一個引用變量」,這兩個操做之間不能重排序。

注意:

final域的重排序規則能夠確保:在引用變量爲任意線程可見以前,該引用變量指向的對象的final域已經在構造函數中被正確初始化過了。其實要獲得這個效果,還須要一個保證:在構造函數內部,不能讓這個被構造對象的引用爲其餘線程可見,也就是對象引用不能在構造函數中「逸出」。

3.2 JMM如何實現final

經過「內存屏障」實現。

final域的寫以後,構造函數return以前,插入一個StoreStore障屏。在讀final域的操做前面插入一個LoadLoad屏障。



第3部分JMM總結

JMM保證:若是程序是正確同步的,程序的執行將具備順序一致性 。

JMM設計

JMM設計者的角度來講,在設計JMM時,須要考慮兩個關鍵因素:

    (01) 程序員對內存模型的使用。程序員但願內存模型易於理解,易於編程。程序員但願基於一個強內存模型(程序儘量的順序執行)來編寫代碼。

    (02) 編譯器和處理器對內存模型的實現。編譯器和處理器但願內存模型對它們的束縛越少越好,這樣它們就能夠作儘量多的優化(對程序重排序,作儘量多的併發)來提升性能。編譯器和處理器但願實現一個弱內存模型。

JMM設計就須要在這二者之間做出協調。JMM對程序採起了不一樣的策略:

    (01) 對於會改變程序執行結果的重排序,JMM要求編譯器和處理器必須禁止這種重排序。

    (02) 對於不會改變程序執行結果的重排序,JMM對編譯器和處理器不做要求(JMM容許這種重排序)。

 


參考文獻

1. 程曉明的「深刻理解Java內存模型」的博客

http://www.infoq.com/cn/articles/java-memory-model-1

2. The JSR-133 Cookbook for Compiler Writers

http://gee.cs.oswego.edu/dl/jmm/cookbook.html

相關文章
相關標籤/搜索