概要
文章是《深入理解Java內容模型》讀書筆記,該書總共包括了3部分的知識。
第1部分,基本概念
包括“並發、同步、主內存、本地內存、重排序、內存屏障、happens before規則、as-if-serial規則、數據依賴性、順序一致性模型、JMM的含義和意義”。
第2部分,同步機制
該部分中就介紹了“同步”的3種方式:volatile、鎖、final。對於每一種方式,從該方式的“特性”、“建立的happens before關系”、“對應的內存語義”、“實現方式”等幾個方面進行了分析說明。實際上,JMM保證“如果程序正確同步,則執行結果與順序一致性內存模型的結果相同”的機制;而這部分這是確保程序正確同步的機制。
第3部分,JMM總結
第1部分 基本概念
1. 並發
定義:即,並發(同時)發生。在操作系統中,是指一個時間段中有幾個程序都處於已啟動運行到運行完畢之間,且這幾個程序都是在同一個處理機上運行,但任一個時刻點上只有一個程序在處理機上運行。
並發需要處理兩個關鍵問題:線程之間如何通信及線程之間如何同步。
(01) 通信 —— 是指線程之間如何交換信息。在命令式編程中,線程之間的通信機制有兩種:共享內存和消息傳遞。
(02) 同步—— 是指程序用於控制不同線程之間操作發生相對順序的機制。在Java中,可以通過volatile,synchronized, 鎖等方式實現同步。
2.主內存和本地內存
主內存 —— 即main memory。在java中,實例域、靜態域和數組元素是線程之間共享的數據,它們存儲在主內存中。
本地內存 —— 即local memory。 局部變量,方法定義參數 和 異常處理器參數是不會在線程之間共享的,它們存儲在線程的本地內存中。
3.重排序
定義:重排序是指“編譯器和處理器”為了提高性能,而在程序執行時會對程序進行的重排序。
說明:重排序分為——“編譯器”和“處理器”兩個方面,而“處理器”重排序又包括“指令級重排序”和“內存的重排序”。
關於重排序,我們需要理解它的思想:為了提高程序的並發度,從而提高性能!但是對於多線程程序,重排序可能會導致程序執行的結果不是我們需要的結果!因此,就需要我們通過“volatile,synchronize,鎖等方式”作出正確的實現同步。
4.內存屏障
定義:包括LoadLoad, LoadStore, StoreLoad, StoreStore共4種內存屏障。內存屏障是與相應的內存重排序相對應的。
屏障類型 |
指令示例 |
說明 |
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線程之間通信的控制機制。
說明:JMM對Java程序作出保證——如果程序是正確同步的,程序的執行將具有順序一致性。即,程序的執行結果與該程序在順序一致性內存模型中的執行結果相同。
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 volatile和 synchronize對比
在功能上,監視器鎖比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變量。根據volatile的happens-before規則,釋放鎖的線程在寫volatile變量之前可見的共享變量,在獲取鎖的線程讀取同一個volatile變量后將立即變的對獲取鎖的線程可見。
非公平鎖
通過CAS實現的,CAS就是compare and swap。CAS實際上調用的JNI函數,也就是CAS依賴於本地實現。以Intel來說,對於CAS的JNI實現函數,它保證:(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