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


 

概要

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

1部分,基本概念

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

2部分,同步機制

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

3部分,JMM總結

 

第1部分 基本概念

1. 並發

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

並發需要處理兩個關鍵問題:線程之間如何通信線程之間如何同步。

(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

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM