Java內存模型淺析

概述java

衡量一個服務性能的高低好壞,每秒事務處理數(Transactions Per Second,TPS)  是最重要的指標之一,它表明着一秒內服務器平均能響應的請求總數,而TPS值與程序的併發能力又有很是密切的關係。數組

Java內存模型(Java Memory Model,JMM)用來屏蔽掉各類硬件和操做系統的內存訪問差別,以實現讓Java程序在各類平臺下都能達到一致的內存訪問效果。Java內存模型的主要目標是定義程序中各個變量的訪問規則,即在虛擬機中將變量存儲到內存和從內存中取出變量這樣的底層細節。緩存

C/C++:直接使用了物理硬件和操做系統的內存模型。安全

硬件的效率與一致性服務器

絕大多數的運算任務都不可能只靠處理器「計算」就能完成,處理器至少要與內存交互,如讀取運算速度、存儲運算結果等,這個I/O操做是很難消除的(沒法僅靠寄存器來完成全部運算任務)。因爲計算機的存儲設備(慢)與處理器的運算速度(快)有幾個數量級的差距,因此現代計算機系統都不得不加入一層讀寫速度儘量接近處理器運算速度的高速緩存(Cache)來做爲內存與處理器之間的緩衝:將運算須要使用到的數據複製到緩存中,讓運算能快速進行,當運算結束後再從緩存同步回內存之中,這樣處理器就無須等待緩慢的內存讀寫了。多線程

緩存一致性(Cache Coherence):在多處理器系統中,每一個處理器都有本身的高速緩存,而它們又共享同一主內存(Main Memory),當多個處理器的運算任務都涉及同一塊主內存區域時,將可能致使各自的緩存數據不一致,若是真的發生這種狀況,那同步回到主內存時以誰的緩存數據爲準呢?爲了解決一致性問題,須要各個處理器訪問緩存時都遵循一些協議,在讀寫時要根據協議來進行操做,這類協議有MSI、MESI(illinois Protocol)  、MOSI、Synapse、Firefly及Dragon Protocol等。「內存模型」一詞,能夠理解爲在特定的操做協議下,對特定的內存或高速緩存進行讀寫訪問的過程抽象。併發

處理器、高速緩存、主內存間的交互關係:app

除了增長高速緩存以外,爲了使得處理器內部的運算單元能儘可能被充分利用,處理器可能會對輸入代碼進行亂序執行(Out-Of-Order Execution)優化,處理器會在計算以後將亂序執行的結果重組,保證該結果與順序執行的結果是一致的,但並不保證程序中各個語句計算的前後順序與輸入代碼中的順序一致,所以,若是存在一個計算任務依賴另一個計算任務的之間結果,那麼其順序性並不能靠代碼的前後順序來保證。與處理器的亂序執行優化相似,Java虛擬機的即時編譯器中也有相似的指令重排序(Instruction Reorder)優化。函數

主內存與工做內存性能

Java內存模型的主要目標是定義程序中各個變量的訪問規則,即在虛擬機中將變量存儲到內存和從內存中取出變量這樣的底層細節。此處的變量(Variables)包括了實例字段、靜態字段和構造數組對象的元素,但不包括局部變量與方法參數,由於後者是線程私有的,不會被共享,天然就不會存在競爭問題。

Java內存模型規定了全部的變量都存儲在主內存(Main Memory)中,每條線程還有本身的工做內存(Working Memory),線程的工做內存中保存了被該線程使用到的變量的主內存副本拷貝,線程對變量的全部操做(讀取、賦值等)都必須在工做內存中進行,而不能直接讀寫主內存中的變量。不一樣的線程之間也沒法直接訪問對方工做內存中的變量,線程間變量值的傳遞均須要經過主內存來完成。

這裏講的主內存、工做內存與Java內存區域中的Java堆、棧、方法區等不是同一個層次的內存劃分,這二者基本上沒有關係,若是必定要勉強對應起來:主內存對應於堆,工做內存對應於棧。從更低層次上說,主內存就直接對應於物理硬件的內存,而爲了得到更好的運行速度,虛擬機(甚至是硬件系統自己的優化措施)可能會讓工做內存優先存儲於寄存器和高速緩存中,由於程序運行時主要訪問讀寫的是工做內存。

線程、主內存、工做內存三者之間的交互關係:

內存間交互操做

1)lock(鎖定):做用於主內存的變量,它把一個變量標識爲一條線程獨佔的狀態。

2)unlock(解鎖):做用於主內存的變量,它把一個處於鎖定狀態的變量釋放出來,釋放後的變量才能夠被其它線程鎖定。

3)read(讀取):做用於主內存的變量,它把一個變量的值從主內存傳輸到線程的工做內存中,以便隨後的load動做使用。

4)load(載入):做用於工做內存的變量,它把read操做從主內存中獲得的變量值放入工做內存的變量副本中。

5)use(使用):做用於工做內存的變量,它把工做內存中一個變量的值傳遞給執行引擎,每當虛擬機遇到一個須要使用到變量的值的字節碼指令時將會執行這個操做。

6)assign(賦值):做用於工做內存的變量,它把一個從執行引擎接收到的值賦給工做內存的變量,每當虛擬機遇到一個給變量賦值的字節碼指令時執行這個操做。

7)store(存儲):做用於工做內存的變量,它把工做內存中一個變量的值傳送給主內存中,以便隨後的write操做使用。

8)write(寫入):做用於主內存的變量,它把store操做從工做內存中獲得的變量的值放入主內存的變量中。

若是要把一個變量從主內存複製到工做內存,那就要順序執行read和load操做,若是要把一個變量從工做內存同步回主內存,就就要順序地執行store和write操做。注意:Java內存模型只要求上述兩個操做必須按順序執行,而沒有保證是連續執行。除此以外,還必須知足:

1)不容許read和load、store和write操做之一單獨出現,即不容許一個變量從主內存讀取了但工做內存不接受,或者從工做內存發起回寫了但主內存不接受的狀況出現。

2)不容許一個線程丟棄它的最近的assign操做,即變量在工做內存中改變了以後必須把該變化同步回主內存。

3)不容許一個線程無緣由地(沒有發生過任何assign操做)把數據從線程的工做內存同步回主內存中。

4)一個新的變量只能在主內存中「誕生」,不容許在工做內存中直接使用一個未被初始化(load或assign)的變量,換句話說,就是對一個變量實施use、store操做以前,必須先執行過了assign和load操做。

5)一個變量在同一時刻只容許一條線程對其進行lock操做,但lock操做能夠被同一條線程重複執行屢次,屢次執行lock後,只有執行相同次數的unlock操做,變量纔會被解鎖。

6)若是對一個變量執行了lock操做,那將會清空工做內存中此變量的值,在執行引擎使用這個變量前, 須要從新執行load或assign操做初始化變量的值。

7)若是一個變量事先沒有被lock操做鎖定,那就不容許對它執行unlock操做,也不容許去unlock一個被其它線程鎖定住的變量。

8)對一個變量執行unlock操做以前,必須把此變量同步回主內存中(執行store、write操做)。

對於volatile型變量的特殊規則

當一個變量定義爲volatile以後,它將具有兩種特性:

1)保證此變量對全部線程的可見性;普通變量(沒被volatile修飾)作不到這一點,普通變量的值在線程間傳遞均須要經過主內存來完成。

2)volatile變量不是線程安全的;volatile變量在各個線程的工做內存中不存在一致性問題(在各個線程的工做內存中,volatile變量也能夠存在不一致的狀況,但因爲每次使用以前都要先刷新,執行引擎看不到不一致的狀況,所以能夠認爲不存在一致性問題),可是Java裏面的運算並不是原子操做,致使volatile變量的運算在併發下同樣是不安全的。

例如:public static volatile int race = 0; race++時,多線程執行會不安全:當getstatic指令把race的值取到操做棧頂時,volatile關鍵字保證了race的值在此時是正確的,可是在執行iconst_一、iadd這些指令的時候,其它線程可能已經把race的值加大了,而在操做棧頂的值就變成了過時的數據,因此putstatic指令執行後就可能把較小的race值同步回主內存之中。

即便編譯出來只有一條字節碼指令,也並不意味執行這條指令就是一個原子操做。

一條字節碼指令在解釋執行時,解釋器將要運行許多行代碼才能實現它的語義,若是是編譯執行,一條字節碼指令也可能轉化成若干條本機機器碼指令。使用-XX:+PrintAssembly參數輸出反彙編來分析更嚴謹。

因爲volatile變量只能保證可見性,在不符合如下兩條規則的運算場景中,須要經過加鎖(Synchronized或java.util.concurrent中的原子類)來保證原子性:

1)運算結果並不依賴變量的當前值,或者可以確保只有單一的線程修改變量的值

2)變量不須要與其它的狀態變量共同參與不變約束

使用Volatile變量的第二個語義是禁止指令重排序優化,普通的變量僅僅會保證在該方法的執行過程當中全部依賴賦值結果的地方都能獲取到正確的結果,而不能保證變量賦值操做的順序與程序代碼中的執行順序一致。由於在一個線程的方法執行過程當中沒法感知到這點,這也就是Java內存模型中描述的所謂的「線程內表現爲串行的語義」(Within-Thread As-If-Serial Semantics)。

指令重排序問題。

在某些狀況下,volatile的同步機制的性能確實要優於鎖(使用Synchronized關鍵字或java.util.concurrent包裏面的鎖),可是因爲虛擬機對鎖實行的許多消除和優化,使得咱們很難量化地認爲volatile就會比Synchronized快多少。若是讓volatile本身與本身比較,那能夠肯定一個原則:volatile變量讀操做的性能消耗與普通變量幾乎沒有差異,可是寫操做則可能會慢一些,由於它須要在本地代碼中插入許多內存屏障指令來保證處理器不發生亂序執行。不過即使如此,大多數場景下volatile的總開銷仍然要比鎖低,咱們在volatile與鎖之中選擇的惟一依據僅僅是volatile的語義可否知足使用場景的需求。

假如T表示一個線程,V和W分別表示兩個volatile型變量,那麼在進行read、load、use���assign、store和write操做時須要知足以下規則:

1)只有當線程T對變量V執行的前一個動做是load的時候,線程T才能對變量V執行use動做;而且,只有當線程T對變量V執行的後一個動做是use的時候,線程T才能對變量V執行load動做。線程T對變量V的use動做能夠認爲是和線程T對變量V的load、read動做相關聯,必須連續一塊兒出現(這條規則要求在工做內存中,每次使用V前都必須先從主內存刷新最新的值,用於保證能看見其它線程對變量V所作的修改後的值)。

2)只有當線程T對變量V執行的前一個動做是assign的時候,線程T才能對變量V執行store動做;而且,只有當線程T對變量V執行的後一個動做是store的時候,線程T才能對變量V執行assign動做。線程T對變量V的assign動做能夠認爲是和線程T對變量V的store、write動做相關聯,必須連續一塊兒出現(這條規則要求在工做內存中,每次修改V後都必須馬上同步回主內存中,用於保證其它線程能夠看到本身對變量V所作的修改)。

3)假定動做A是線程T對變量V實施的use或assign動做,假定動做F是和動做A相關聯的load或store動做,假定動做P是和動做F相應的對變量V的read或write動做;相似的, 假定動做B是線程T對變量W實施的use或assign動做,假定動做G是和動做B相關聯的load或store動做,假定動做Q是和動做G相應的對變量W的read或write動做。若是A先於B,那麼P先於Q(這條規則要求volatile修飾的變量不會被指令重排序優化,保證代碼的執行順序與程序的順序相同)。

對於long和double型變量的特殊規則

Java內存模型要求lock、unlock、read、load、assign、use、store、write這8個操做都具備原子性,可是對於64位的數據類型(long和double),在模型中特別定義了一條相對寬鬆的規定:容許虛擬機將沒有被volatile修飾的64位數據的讀寫操做劃分爲兩次32位的操做來進行,即容許虛擬機實現選擇能夠不保證64位數據類型的load、store、read和write這4個操做的原子性,這點就是所謂的long和double的非原子性協定(Not atomic Treatment of double and long Variables)。

若是有多個線程共享一個並未聲明爲volatile的long或double類型的變量,而且同時對它們進行讀取和修改操做,那麼某些線程可能會讀取到一個即非原值,也不是其它線程修改值得表明了「半個變量」的數值。

Java內存模型雖然容許虛擬機不把long和double變量的讀寫實現成原子操做,但容許虛擬機選擇把這些操做實現爲具備原子性的操做,並且還「強烈建議」虛擬機這樣實現。在實際開發中,目前各類平臺下的商用虛擬機幾乎都選擇把64位數據的讀寫操做做爲原子操做來對待,所以咱們在編寫代碼時通常不須要把用到的long和double變量專門聲明爲volatile。

原子性、可見性和有序性

原子性(Atomicity):由Java內存模型來直接保證的原子性變量操做包括read、load、assign、use、store、write,咱們大體能夠認爲基本數據類型的訪問讀寫是具有原子性的(例外就是long和double的非原子性協定)。

Java內存模型還提供了lock和unlock操做來知足這種需求,儘管虛擬機未把lock和unlock操做直接開放給用戶使用,可是卻提供了更高層次的字節碼指令monitorenter和monitorexit來隱式地使用這兩個操做,這兩個字節碼指令反映到Java代碼中就是同步塊--Synchronized關鍵字,所以在Synchronized塊之間的操做也具有原子性。

可見性(Visibility):可見性是指當一個線程修改了共享變量的值,其它線程可以當即得知這個修改。Java內存模型是經過在變量修改後將新值同步回主內存,在變量讀取前從主內存刷新變量值這種依賴主內存做爲傳遞媒介的方法來實現可見性的,不管是普通變量仍是volatile變量都是如此,普通變量與volatile變量的區別是,volatile的特殊規則保證了新值能當即同步到主內存,以及每次使用前當即從主內存刷新。所以,能夠說volatile保證了多線程操做時變量的可見性,而普通變量則不能保證這一點。

除了volatile以外,Java中Synchronized和final也能實現可見性。同步塊的可見性是由「對一個變量執行unlock操做以前,必須先把此變量同步回主內存中(執行store、write操做)」這條規則得到的,final關鍵字的可見性是指:被final修飾的字段在構造器中一旦初始化完成,而且構造器沒有把「this」的引用傳遞出去(this引用逃逸是一件很危險的事情,其它線程有可能經過這個引用訪問到「初始化了一半」的對象),那在其它線程中就能看見final字段的值。

有序性(Ordering):若是在本線程內觀察,全部的操做都是有序的;若是在一個線程中觀察另外一個線程,全部的操做都是無序的。前半句是指「線程內表現爲串行的語義」(Within-Thread As-If-Serial Semantics),後半句是指「指令重排序」現象和「工做內存與主內存同步延遲」現象。

Java中volatile和Synchronized這兩個關鍵字保證線程之間操做的有序性,volatile關鍵字自己就包含了禁止指令重排序的語義,而Synchronized則是由「一個變量在同一時刻只容許一條線程對其進行lock操做」這條規則得到的,這條規則決定了持有同一個鎖的兩個同步塊只能串行地進入。

Synchronized關鍵字在須要這3種特性的時候均可以做爲其中一種的解決方案。

先行發生(happens-before)原則

這個原則很是重要,它是判斷數據是否存在競爭、線程是否安全的主要依據,依靠這個原則,咱們能夠經過幾條規則一攬子地解決併發環境下兩個操做之間是否可能存在衝突的全部問題。

先行發生是Java內存模型中定義的兩項操做之間的偏序關係,若是說操做A先行發生於操做B,其實就是說在發生操做B以前,操做A產生的影響能被操做B觀察到,「影響」包括修改了內存中共享變量的值、發送了消息、調用了方法等。

程序次序規則(Program Order Rule):在一個線程內,按照程序代碼順序,書寫在前面的操做先行發生於書寫在後面的操做。準確地說,應該是控制流順序而不是程序代碼順序,由於要考慮分支,循環等結構。

管程鎖定規則(Monitor Lock Rule):一個unlock操做先行發生於後面對同一個鎖的lock操做。這裏必須強調的是同一個鎖,而「後面」是指時間上的前後順序。

volatile變量規則(Volatile Variable Rule):對一個volatile變量的寫操做先行發生於後面對這個變量的讀操做,這裏的「後面」一樣是指時間上的前後順序。

線程啓動規則(Thread Start Rule):Thread對象的start()方法先行發生於此線程的每個動做。

線程終止規則(Thread Termination Rule):線程中的全部操做都先行發生於對此線程的終止檢測,咱們能夠經過Thread.join()方法結束、Thread.isAlive()的返回值等手段檢測到程序已經終止執行。

線程中斷規則(Thread Interrupting Rule):對線程interrupt()方法的調用先行發生於被中斷線程的代碼檢測到中斷事件的發生,能夠經過Thread.interrupted()方法檢測到是否有中斷髮生。

對象終結規則(Finalizer Rule):一個對象的初始化完成(構造函數執行結束)先行發生於ta的finalize()方法的開始。

傳遞性(Transitivity):若是操做A先行發生於操做B,操做B先行發生於操做C,那就能夠得出操做A先行發生於操做C的結論。

Java語言無需任何同步手段保障就能成立的先行發生規則就只有上面這些了。

一個操做「時間上的先發生」不表明這個操做會是「先行發生」,那若是一個操做「先行發生」是否就能推導出這個操做一定是「時間上的先發生」呢?很遺憾,這個推論也是不成立的,一個典型的例子就是屢次提到的「指令重排序」。

時間前後順序與先行發生原則之間基本沒有太大的關係,因此咱們衡量併發安全問題的時候不要受到時間順序的干擾,一切必須以先行發生原則爲準。

相關文章
相關標籤/搜索