Java內存模型與線程java
概述windows
多任務處理在現代計算機操做系統中幾乎已經是一項必備的功能,多任務運行是壓榨手段,就如windows同樣,咱們使勁的壓榨它運行多個任務,俱要high又要耍。併發則是另一種更具體的應用場景。每秒事物處理數(Transactions per Second,tps)是最重要的指標。開發人員應該瞭解與運用併發。緩存
硬件的效率與一致性安全
除了有軟件上的併發,物理計算機也有併發問題。計算機的存儲設備與處理器運算速度有幾個數量級的差距,現代計算機都不得不加入一層高速緩存來做爲內存與處理器之間的緩衝,這樣可以提高處理速度。基於高速緩存解決了處理器與內存的速度矛盾,可是也提升了計算機系統複雜度,帶來了緩存一致性問題。在多處理器系統中,每一個處理器有本身的高速緩存,而它們又共享同一個主內存。如圖:多線程
多個處理器的任務都涉及同一塊主內存區域時,將可能致使各自的緩存數據不一致。爲了解決這種一致性的問題,須要各個處理器訪問緩存時都遵循一些協議,在操做時要根據協議來進行操做,這類協議有MSI、MESI(Illinois Protoclo)、MOSI、Synapse、Firefly以及Dragon Protocal等。併發
除了增長高速緩存以外,爲了使處理器獲得充分利用,處理器可能會對輸入代碼進行亂序執行(Out-of-Order Execution)優化,處理器以後會對亂序執行的結果重組,保證與順序執行的結果是一致的,不保證各個語句計算的前後順序因爲輸入代碼中順序一致。這樣致使的結果是一個計算任務依賴於另一個計算任務的中間結果,其順序性不能依靠代碼的順序性來保存。Java中也存在相似的指令重排序(Instruction Reorder)優化。app
java內存模型ide
Java虛擬機規範中試圖定義一直java內存模型來屏蔽各類硬件和操做系統的內存訪問差別,以實現讓java程序在各類平臺下都可以達到一致的內存訪問效果。定義內存模型不是一件容易的事情,要足夠嚴謹足夠寬鬆。嚴謹是爲併發內存操做不會產生歧義,寬鬆是爲有足夠空間去利用各類硬件的特性。性能
主內存與工做內存優化
Java內存模型規定全部的變量的存儲在主內存中,主內存是java虛擬機內存的一部分,每一個線程還有本身的工做內存。線程的工做內存保存了被該線程使用到的變量的主內存副本拷貝,線程對變量的全部操做都必須在工做內存中進行,而不能直接操做主內存。不一樣線程之間相互隔離。線程間變量值的傳遞須要經過主內存完成。三者關係以下:
這裏的主內存、工做內存與Java內存區域的Java堆、棧、方法區不是同一層次內存劃分。
內存間交互操做
關於主內存與工做內存之間具體的交互協議,java內存模型定義瞭如下8種操做來完成。
lock(鎖定):做用於主內存的變量,把一個變量標識爲一條線程獨佔狀態。
unlock(解鎖):做用於主內存變量,把一個處於鎖定狀態的變量釋放出來,釋放後的變量才能夠被其餘線程鎖定。
read(讀取):做用於主內存變量,把 一個變量值從主內存傳輸到線程的工做內存中,以便隨後的load動做使用
load(載入):做用於工做內存的變量,它把read操做從主內存中獲得的變量值放入工做內存的變量副本中。
use(使用):做用於工做內存的變量,把工做內存中的一個變量值傳遞給執行引擎,每當虛擬機遇到一個須要使用變量的值的字節碼指令時將會執行這個操做。
assign(賦值):做用於工做內存的變量,它把一個從執行引擎接收到的值賦值給工做內存的變量,每當虛擬機遇到一個給變量賦值的字節碼指令時執行這個操做。
store(存儲):做用於工做內存的變量,把工做內存中的一個變量的值傳送到主內存中,以便隨後的write的操做。
write(寫入):做用於主內存的變量,它把store操做從工做內存中一個變量的值傳送到主內存的變量中。
若是要把一個變量從主內存中複製到工做內存,就須要按順尋地執行read和load操做,若是把變量從工做內存中同步回主內存中,就要按順序地執行store和write操做。Java內存模型只要求上述操做必須按順序執行,而沒有保證必須是連續執行。也就是read和load之間,store和write之間是能夠插入其餘指令的,如對主內存中的變量a、b進行訪問時,可能的順序是read a,read b,load b, load a。Java內存模型還規定了在執行上述八種基本操做時,必須知足以下規則:
① 不容許read和load、store和write操做之一單獨出現
② 不容許一個線程丟棄它的最近assign的操做,即變量在工做內存中改變了以後必須同步到主內存中。
③ 不容許一個線程無緣由地(沒有發生過任何assign操做)把數據從工做內存同步回主內存中。
④ 一個新的變量只能在主內存中誕生,不容許在工做內存中直接使用一個未被初始化(load或assign)的變量。即就是對一個變量實施use和store操做以前,必須先執行過了assign和load操做。
⑤ 一個變量在同一時刻只容許一條線程對其進行lock操做,lock和unlock必須成對出現
⑥ 若是對一個變量執行lock操做,將會清空工做內存中此變量的值,在執行引擎使用這個變量前須要從新執行load或assign操做初始化變量的值
⑦ 若是一個變量事先沒有被lock操做鎖定,則不容許對它執行unlock操做;也不容許去unlock一個被其餘線程鎖定的變量。
⑧ 對一個變量執行unlock操做以前,必須先把此變量同步到主內存中(執行store和write操做)。
對於volatitle型變量的特殊規則
關鍵字volatitle是java虛擬機提供的輕量級的同步機制,正確、完整的理解它有難度。瞭解volatitle關鍵字對了解多線程操做的其餘特徵頗有意義。volatitle關鍵字定義的變量有2種特性:
第一種保證此變量對全部線程的可見性。
第二種是禁止指令重排序優化。
volatitle變量只能保證可見性,在不符合如下2條規則的運算場景中,咱們仍然須要經過加鎖來保證原子性:
① 運算結果不依賴變量的當前值,或者可以保證只有單一的線程修改變量的值。
② 變量不須要與其餘的狀態變量共同參與不變約束。
volatitle修飾的變量關鍵變化是多執行了一個「lock addl $0x0,(%esp)」操做,這個操做至關於一個內存屏障(Memory Barrier或Memory Fence,重排序不能把後面的指令重排序到內存屏障位置以前)
爲何選擇volatitle(在必定狀況下)?
在讀操做的性能消耗與普通變量幾乎沒什麼區別,在寫操做慢一些。虛擬機對鎖有許多消除與優化。volatitle關鍵字就介於普通變量與鎖之間。
對於long和double型變量的特殊規則
Java內存模型lock、unlock、read、load、assign、user、store、write這8個操做都有原子性,可是java內存模型將沒有被volatile修飾的64位的數據的讀寫操做劃分爲兩次32爲的操做來進行,這樣的話,多線程併發,就會存在線程可能讀取到「半個變量」的值,不過,這種狀況很是罕見,目前各平臺的商用虛擬機幾乎都選擇把64位的讀寫做爲原子操做來實現規範的。
原子性、可見性與有序性
原子性:原子性是指在一個操做中就是cpu不能夠在中途暫停而後再調度,既不被中斷操做,要不執行完成,要不就不執行。
可見性:可見性就是指當一個線程修改了線程共享變量的值,其它線程可以當即得知這個修改。
有序性:Java內存模型中的程序自然有序性能夠總結爲一句話:若是在本線程內觀察,全部操做都是有序的;若是在一個線程中觀察另外一個線程,全部操做都是無序的。前半句是指「線程內表現爲串行語義」,後半句是指「指令重排序」現象和「工做內存中主內存同步延遲」現象。
先行發生原則(Happens-Before)
先行發生原則(Happens-Before)是判斷數據是否存在競爭、線程是否安全的主要依據。先行發生是Java內存,模型中定義的兩項操做之間的偏序關係,若是操做A先行發生於操做B,那麼操做A產生的影響可以被操做B觀察到。
Java內存模型中存在的自然的先行發生關係:
1. 程序次序規則:同一個線程內,按照代碼出現的順序,前面的代碼先行於後面的代碼,準確的說是控制流順序,由於要考慮到分支和循環結構。
2. 管程鎖定規則:一個unlock操做先行發生於後面(時間上)對同一個鎖的lock操做。
3. volatile變量規則:對一個volatile變量的寫操做先行發生於後面(時間上)對這個變量的讀操做。
4. 線程啓動規則:Thread的start( )方法先行發生於這個線程的每個操做。
5. 線程終止規則:線程的全部操做都先行於此線程的終止檢測。能夠經過Thread.join( )方法結束、Thread.isAlive( )的返回值等手段檢測線程的終止。
6. 線程中斷規則:對線程interrupt( )方法的調用先行發生於被中斷線程的代碼檢測到中斷事件的發生,能夠經過Thread.interrupt( )方法檢測線程是否中斷
7. 對象終結規則:一個對象的初始化完成先行於發生它的finalize()方法的開始。
8. 傳遞性:若是操做A先行於操做B,操做B先行於操做C,那麼操做A先行於操做C。
總結:一個操做「時間上的先發生」不表明這個操做先行發生;一個操做先行發生也不表明這個操做在時間上是先發生的(重排序的出現)。時間上的前後順序對先行發生沒有太大的關係,因此衡量併發安全問題的時候不要受到時間順序的影響,一切以先行發生原則爲準。