Java入門教程-深入理解Java內(nèi)存模型
概要
第1部分,基本概念
包括“并發(fā)、同步、主內(nèi)存、本地內(nèi)存、重排序、內(nèi)存屏障、happensbefore規(guī)則、as-if-serial規(guī)則、數(shù)據(jù)依賴性、順序一致性模型、JMM的含義和意義”。
第2部分,同步機制
該部分中就介紹了“同步”的3種方式:volatile、鎖、final。對于每一種方式,從該方式的“特性”、“建立的happensbefore關(guān)系”、“對應(yīng)的內(nèi)存語義”、“實現(xiàn)方式”等幾個方面進行了分析說明。實際上,JMM保證“如果程序正確同步,則執(zhí)行結(jié)果與順序一致性內(nèi)存模型的結(jié)果相同”的機制;而這部分這是確保程序正確同步的機制。
第3部分,JMM總結(jié)
第1部分 基本概念
1. 并發(fā)
定義:即,并發(fā)(同時)發(fā)生。在操作系統(tǒng)中,是指一個時間段中有幾個程序都處于已啟動運行到運行完畢之間,且這幾個程序都是在同一個處理機上運行,但任一個時刻點上只有一個程序在處理機上運行。
并發(fā)需要處理兩個關(guān)鍵問題:線程之間如何通信及線程之間如何同步。
(01) 通信 —— 是指線程之間如何交換信息。在命令式編程中,線程之間的通信機制有兩種:共享內(nèi)存和消息傳遞。
(02) 同步—— 是指程序用于控制不同線程之間操作發(fā)生相對順序的機制。在Java中,可以通過volatile,synchronized, 鎖等方式實現(xiàn)同步。
2.主內(nèi)存和本地內(nèi)存
主內(nèi)存? ? ?—— 即main memory。在java中,實例域、靜態(tài)域和數(shù)組元素是線程之間共享的數(shù)據(jù),它們存儲在主內(nèi)存中。
本地內(nèi)存 —— 即local memory。 局部變量,方法定義參數(shù) 和 異常處理器參數(shù)是不會在線程之間共享的,它們存儲在線程的本地內(nèi)存中。
3.重排序
定義:重排序是指“編譯器和處理器”為了提高性能,而在程序執(zhí)行時會對程序進行的重排序。
說明:重排序分為——“編譯器”和“處理器”兩個方面,而“處理器”重排序又包括“指令級重排序”和“內(nèi)存的重排序”。
關(guān)于重排序,我們需要理解它的思想:為了提高程序的并發(fā)度,從而提高性能!但是對于多線程程序,重排序可能會導(dǎo)致程序執(zhí)行的結(jié)果不是我們需要的結(jié)果!因此,就需要我們通過“volatile,synchronize,鎖等方式”作出正確的實現(xiàn)同步。
4.內(nèi)存屏障
定義:包括LoadLoad, LoadStore, StoreLoad, StoreStore共4種內(nèi)存屏障。內(nèi)存屏障是與相應(yīng)的內(nèi)存重排序相對應(yīng)的。

5. happens-before
定義:JDK5(JSR-133)提供的概念,用于描述多線程操作之間的內(nèi)存可見性。如果一個操作執(zhí)行的結(jié)果需要對另一個操作可見,那么這兩個操作之間必須存在happens-before關(guān)系。
作用:描述多線程操作之間的內(nèi)存可見性。
? ? ? ? ?[程序順序規(guī)則]:一個線程中的每個操作,happens- before 于該線程中的任意后續(xù)操作。
? ? ? ? ?[監(jiān)視器鎖規(guī)則]:對一個監(jiān)視器鎖的解鎖,happens- before 于隨后對這個監(jiān)視器鎖的加鎖。
? ? ? ? ?[volatile變量規(guī)則]:對一個volatile域的寫,happens- before 于任意后續(xù)對這個volatile域的讀。
? ? ? ? ?[傳遞性]:如果A happens- before B,且B happens- before C,那么A happens- before C。
6. 數(shù)據(jù)依賴性
定義:如果兩個操作訪問同一個變量,且這兩個操作中有一個為寫操作,此時這兩個操作之間就存在數(shù)據(jù)依賴性。
作用:編譯器和處理器不會對“存在數(shù)據(jù)依賴關(guān)系的兩個操作”執(zhí)行重排序。
7.as-if-serial
定義:不管怎么重排序,程序的執(zhí)行結(jié)果不能被改變。
8. 順序一致性內(nèi)存模型
定義:它是理想化的內(nèi)存模型。有以下規(guī)則:
? ? ? ? (01) 一個線程中的所有操作必須按照程序的順序來執(zhí)行。
? ? ? ? (02) 所有線程都只能看到一個單一的操作執(zhí)行順序。在順序一致性內(nèi)存模型中,每個操作都必須原子執(zhí)行且立刻對所有線程可見。
9. JMM
定義:Java Memory Mode,即Java內(nèi)存模型。它是Java線程之間通信的控制機制。
說明:JMM對Java程序作出保證——如果程序是正確同步的,程序的執(zhí)行將具有順序一致性。即,程序的執(zhí)行結(jié)果與該程序在順序一致性內(nèi)存模型中的執(zhí)行結(jié)果相同。
10. 可見性
可見性一般用于指不同線程之間的數(shù)據(jù)是否可見。
在java中, 實例域、靜態(tài)域和數(shù)組元素這些數(shù)據(jù)是線程之間共享的數(shù)據(jù),它們存儲在主內(nèi)存中;主內(nèi)存中的所有數(shù)據(jù)對該內(nèi)存中的線程都是可見的。而局部變量,方法定義參數(shù) 和 異常處理器參數(shù)這些數(shù)據(jù)是不會在線程之間共享的,它們存儲在線程的本地內(nèi)存中;它們對其它線程是不可見的。
此外,對于主內(nèi)存中的數(shù)據(jù),在本地內(nèi)存中會對應(yīng)的創(chuàng)建該數(shù)據(jù)的副本(相當于緩沖);這些副本對于其它線程也是不可見的。
11. 原子性
是指一個操作是按原子的方式執(zhí)行的。要么該操作不被執(zhí)行;要么以原子方式執(zhí)行,即執(zhí)行過程中不會被其它線程中斷。
第2部分 同步機制
1.volatile
1.1 作用
volatile變量自身具有下列特性:
? ? ? [可見性]:對一個volatile變量的讀,總是能看到(任意線程)對這個volatile變量最后的寫入。
1.2 volatile的內(nèi)存語義
volatile寫:當寫一個volatile變量時,JMM會把該線程對應(yīng)的本地內(nèi)存中的共享變量刷新到主內(nèi)存。
volatile讀:當讀一個volatile變量時,JMM會把該線程對應(yīng)的本地內(nèi)存置為無效。線程接下來將從主內(nèi)存中讀取共享變量。
1.3 JMM中的實現(xiàn)方式
JMM針對編譯器制定的volatile重排序規(guī)則表:

下面是基于保守策略的JMM內(nèi)存屏障插入策略:
在每個volatile寫操作的前面插入一個StoreStore屏障。
在每個volatile寫操作的后面插入一個StoreLoad屏障。
在每個volatile讀操作的后面插入一個LoadLoad屏障。
在每個volatile讀操作的后面插入一個LoadStore屏障。
1.4 volatile和 synchronize對比
在功能上,監(jiān)視器鎖比volatile更強大;在可伸縮性和執(zhí)行性能上,volatile更有優(yōu)勢。
volatile僅僅保證對單個volatile變量的讀/寫具有原子性;而synchronize鎖的互斥執(zhí)行的特性可以確保對整個臨界區(qū)代碼的執(zhí)行具有原子性。
2.鎖
2.1 作用
鎖是java并發(fā)編程中最重要的同步機制。
2.2 鎖的內(nèi)存語義
? ? ?(01) 線程A釋放一個鎖,實質(zhì)上是線程A向接下來將要獲取這個鎖的某個線程發(fā)出了(線程A對共享變量所做修改的)消息。
? ? ?(02) 線程B獲取一個鎖,實質(zhì)上是線程B接收了之前某個線程發(fā)出的(在釋放這個鎖之前對共享變量所做修改的)消息。
? ? ?(03) 線程A釋放鎖,隨后線程B獲取這個鎖,這個過程實質(zhì)上是線程A通過主內(nèi)存向線程B發(fā)送消息。
2.3 JMM如何實現(xiàn)鎖
公平鎖
公平鎖是通過“volatile”實現(xiàn)同步的。公平鎖在釋放鎖的最后寫volatile變量state;在獲取鎖時首先讀這個volatile變量。根據(jù)volatile的happens-before規(guī)則,釋放鎖的線程在寫volatile變量之前可見的共享變量,在獲取鎖的線程讀取同一個volatile變量后將立即變的對獲取鎖的線程可見。
非公平鎖
通過CAS實現(xiàn)的,CAS就是compare and swap。CAS實際上調(diào)用的JNI函數(shù),也就是CAS依賴于本地實現(xiàn)。以Intel來說,對于CAS的JNI實現(xiàn)函數(shù),它保證:(01)禁止該CAS之前和之后的讀和寫指令重排序。(02)把寫緩沖區(qū)中的所有數(shù)據(jù)刷新到內(nèi)存中。
3.final
3.1 特性
對于基本類型的final域,編譯器和處理器要遵守兩個重排序規(guī)則:
(01) final寫:“構(gòu)造函數(shù)內(nèi)對一個final域的寫入”,與“隨后把這個被構(gòu)造對象的引用賦值給一個引用變量”,這兩個操作之間不能重排序。
(02) final讀:“初次讀一個包含final域的對象的引用”,與“隨后初次讀對象的final域”,這兩個操作之間不能重排序。
對于引用類型的final域,除上面兩條之外,還有一條規(guī)則:
(03) final寫:在“構(gòu)造函數(shù)內(nèi)對一個final引用的對象的成員域的寫入”,與“隨后在構(gòu)造函數(shù)外把這個被構(gòu)造對象的引用賦值給一個引用變量”,這兩個操作之間不能重排序。
注意:
寫final域的重排序規(guī)則可以確保:在引用變量為任意線程可見之前,該引用變量指向的對象的final域已經(jīng)在構(gòu)造函數(shù)中被正確初始化過了。其實要得到這個效果,還需要一個保證:在構(gòu)造函數(shù)內(nèi)部,不能讓這個被構(gòu)造對象的引用為其他線程可見,也就是對象引用不能在構(gòu)造函數(shù)中“逸出”。
3.2 JMM如何實現(xiàn)final
通過“內(nèi)存屏障”實現(xiàn)。
在final域的寫之后,構(gòu)造函數(shù)return之前,插入一個StoreStore障屏。在讀final域的操作前面插入一個LoadLoad屏障。
第3部分JMM總結(jié)
JMM保證:如果程序是正確同步的,程序的執(zhí)行將具有順序一致性 。
JMM設(shè)計
從JMM設(shè)計者的角度來說,在設(shè)計JMM時,需要考慮兩個關(guān)鍵因素:
? ? (01) 程序員對內(nèi)存模型的使用。程序員希望內(nèi)存模型易于理解,易于編程。程序員希望基于一個強內(nèi)存模型(程序盡可能的順序執(zhí)行)來編寫代碼。
? ? (02) 編譯器和處理器對內(nèi)存模型的實現(xiàn)。編譯器和處理器希望內(nèi)存模型對它們的束縛越少越好,這樣它們就可以做盡可能多的優(yōu)化(對程序重排序,做盡可能多的并發(fā))來提高性能。編譯器和處理器希望實現(xiàn)一個弱內(nèi)存模型。
JMM設(shè)計就需????要在這兩者之間作出協(xié)調(diào)。JMM對程序采取了不同的策略:
? ? (01) 對于會改變程序執(zhí)行結(jié)果的重排序,JMM要求編譯器和處理器必須禁止這種重排序。
? ? (02) 對于不會改變程序執(zhí)行結(jié)果的重排序,JMM對編譯器和處理器不作要求(JMM允許這種重排序)。