文章是《深刻理解Java內容模型》讀書筆記,該書總共包括了3部分的知識。html
第1部分,基本概念
java
包括「併發、同步、主內存、本地內存、重排序、內存屏障、happens before規則、as-if-serial規則、數據依賴性、順序一致性模型、JMM的含義和意義」。程序員
第2部分,同步機制編程
該部分中就介紹了「同步」的3種方式:volatile、鎖、final。對於每一種方式,從該方式的「特性」、「創建的happens before關係」、「對應的內存語義」、「實現方式」等幾個方面進行了分析說明。實際上,JMM保證「若是程序正確同步,則執行結果與順序一致性內存模型的結果相同」的機制;而這部分這是確保程序正確同步的機制。數組
第3部分,JMM總結多線程
1. 併發併發
定義:即,併發(同時)發生。在操做系統中,是指一個時間段中有幾個程序都處於已啓動運行到運行完畢之間,且這幾個程序都是在同一個處理機上運行,但任一個時刻點上只有一個程序在處理機上運行。app
併發須要處理兩個關鍵問題:線程之間如何通訊及線程之間如何同步。函數
(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. 原子性
是指一個操做是按原子的方式執行的。要麼該操做不被執行;要麼以原子方式執行,即執行過程當中不會被其它線程中斷。
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屏障。
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