多任務和高併發是衡量一臺計算機處理器的能力重要指標之一。通常衡量一個服務器性能的高低好壞,使用每秒事務處理數(Transactions Per Second,TPS)這個指標比較能說明問題,它表明着一秒內服務器平均能響應的請求數,而TPS值與程序的併發能力有着很是密切的關係。物理機的併發問題與虛擬機中的狀況有不少類似之處,物理機對併發的處理方案對於虛擬機的實現也有至關大的參考意義。java
因爲計算機的存儲設備與處理器的運算能力之間有幾個數量級的差距,因此現代計算機系統都不得不加入一層讀寫速度儘量接近處理器運算速度的高速緩存(cache)來做爲內存與處理器之間的緩衝:將運算須要使用到的數據複製到緩存中,讓運算能快速進行,當運算結束後再從緩存同步回內存之中,這樣處理器就無需等待緩慢的內存讀寫了。程序員
基於高速緩存的存儲交互很好地解決了處理器與內存的速度矛盾,可是引入了一個新的問題:緩存一致性(Cache Coherence)。在多處理器系統中,每一個處理器都有本身的高速緩存,而他們又共享同一主存,以下圖所示:多個處理器運算任務都涉及同一塊主存,須要一種協議能夠保障數據的一致性,這類協議有MSI、MESI、MOSI及Dragon Protocol等。編程
除此以外,爲了使得處理器內部的運算單元能儘量被充分利用,處理器可能會對輸入代碼進行亂序執行(Out-Of-Order Execution)優化,處理器會在計算以後將對亂序執行的代碼進行結果重組,保證結果準確性。與處理器的亂序執行優化相似,Java虛擬機的即時編譯器中也有相似的指令重排序(Instruction Recorder)優化。數組
內存模型能夠理解爲在特定的操做協議下,對特定的內存或者高速緩存進行讀寫訪問的過程抽象,不一樣架構下的物理機擁有不同的內存模型,Java虛擬機也有本身的內存模型,即Java內存模型(Java Memory Model, JMM)。在C/C++語言中直接使用物理硬件和操做系統內存模型,致使不一樣平臺下併發訪問出錯。而JMM的出現,可以屏蔽掉各類硬件和操做系統的內存訪問差別,實現平臺一致性,是的Java程序可以「一次編寫,處處運行」。緩存
Java內存模型的主要目標是定義程序中各個變量的訪問規則,即在虛擬機中將變量存儲到內存和從內存中取出變量這樣底層細節。此處的變量與Java編程時所說的變量不同,指包括了實例字段、靜態字段和構成數組對象的元素,可是不包括局部變量與方法參數,後者是線程私有的,不會被共享。安全
Java內存模型中規定了全部的變量都存儲在主內存中,每條線程還有本身的工做內存(能夠與前面講的處理器的高速緩存類比),線程的工做內存中保存了該線程使用到的變量到主內存副本拷貝,線程對變量的全部操做(讀取、賦值)都必須在工做內存中進行,而不能直接讀寫主內存中的變量。不一樣線程之間沒法直接訪問對方工做內存中的變量,線程間變量值的傳遞均須要在主內存來完成,線程、主內存和工做內存的交互關係以下圖所示,和上圖很相似。服務器
注意:這裏的主內存、工做內存與Java內存區域的Java堆、棧、方法區不是同一層次內存劃分,這二者基本上沒有關係。多線程
由上面的交互關係可知,關於主內存與工做內存之間的具體交互協議,即一個變量如何從主內存拷貝到工做內存、如何從工做內存同步到主內存之間的實現細節,Java內存模型定義瞭如下八種操做來完成:架構
若是要把一個變量從主內存中複製到工做內存,就須要按順尋地執行read和load操做,若是把變量從工做內存中同步回主內存中,就要按順序地執行store和write操做。Java內存模型只要求上述兩個操做必須按順序執行,而沒有保證必須是連續執行。也就是read和load之間,store和write之間是能夠插入其餘指令的,如對主內存中的變量a、b進行訪問時,可能的順序是read a,read b,load b, load a。Java內存模型還規定了在執行上述八種基本操做時,必須知足以下規則:併發
這8種內存訪問操做很繁瑣,後文會使用一個等效判斷原則,即先行發生(happens-before)原則來肯定一個內存訪問在併發環境下是否安全。
關鍵字volatile是JVM中最輕量的同步機制。volatile變量具備2種特性:
volatile語義並不能保證變量的原子性。對任意單個volatile變量的讀/寫具備原子性,但相似於i++、i–這種複合操做不具備原子性,由於自增運算包括讀取i的值、i值增長一、從新賦值3步操做,並不具有原子性。
因爲volatile只能保證變量的可見性和屏蔽指令重排序,只有知足下面2條規則時,才能使用volatile來保證併發安全,不然就須要加鎖(使用synchronized、lock或者java.util.concurrent中的Atomic原子類)來保證併發中的原子性。
由於須要在本地代碼中插入許多內存屏蔽指令在屏蔽特定條件下的重排序,volatile變量的寫操做與讀操做相比慢一些,可是其性能開銷比鎖低不少。
JMM要求lock、unlock、read、load、assign、use、store、write這8個操做都必須具備原子性,但對於64爲的數據類型(long和double,具備非原子協定:容許虛擬機將沒有被volatile修飾的64位數據的讀寫操做劃分爲2次32位操做進行。(與此相似的是,在棧幀結構的局部變量表中,long和double類型的局部變量可使用2個能存儲32位變量的變量槽(Variable Slot)來存儲的,關於這一部分的詳細分析,詳見詳見周志明著《深刻理解Java虛擬機》8.2.1節)
若是多個線程共享一個沒有聲明爲volatile的long或double變量,而且同時讀取和修改,某些線程可能會讀取到一個既非原值,也不是其餘線程修改值的表明了「半個變量」的數值。不過這種狀況十分罕見。由於非原子協議換句話說,一樣容許long和double的讀寫操做實現爲原子操做,而且目前絕大多數的虛擬機都是這樣作的。
JMM保證的原子性變量操做包括read、load、assign、use、store、write,而long、double非原子協定致使的非原子性操做基本能夠忽略。若是須要對更大範圍的代碼實行原子性操做,則須要JMM提供的lock、unlock、synchronized等來保證。
前面分析volatile語義時已經提到,可見性是指當一個線程修改了共享變量的值,其餘線程可以當即得知這個修改。JMM在變量修改後將新值同步回主內存,依賴主內存做爲媒介,在變量被線程讀取前從內存刷新變量新值,保證變量的可見性。普通變量和volatile變量都是如此,只不過volatile的特殊規則保證了這種可見性是當即得知的,而普通變量並不具有這種嚴格的可見性。除了volatile外,synchronized和final也能保證可見性。
JMM的有序性表現爲:若是在本線程內觀察,全部的操做都是有序的;若是在一個線程中觀察另外一個線程,全部的操做都是無序的。前半句指「線程內表現爲串行的語義」(as-if-serial),後半句值「指令重排序」和普通變量的」工做內存與主內存同步延遲「的現象。
在執行程序時爲了提升性能,編譯器和處理器常常會對指令進行重排序。從硬件架構上來講,指令重排序是指CPU採用了容許將多條指令不按照程序規定的順序,分開發送給各個相應電路單元處理,而不是指令任意重排。重排序分紅三種類型:
從Java源代碼到最終實際執行的指令序列,會通過三種重排序。可是,爲了保證內存的可見性,Java編譯器在生成指令序列的適當位置會插入內存屏障指令來禁止特定類型的處理器重排序。對於編譯器的重排序,JMM會根據重排序規則禁止特定類型的編譯器重排序;對於處理器重排序,JMM會插入特定類型的內存屏障,經過內存的屏障指令禁止特定類型的處理器重排序。這裏討論JMM對處理器的重排序,爲了更深理解JMM對處理器重排序的處理,先來認識一下常見處理器的重排序規則:
其中的N標識處理器不容許兩個操做進行重排序,Y表示容許。其中Load-Load表示讀-讀操做、Load-Store表示讀-寫操做、Store-Store表示寫-寫操做、Store-Load表示寫-讀操做。能夠看出:常見處理器對寫-讀操做都是容許重排序的,而且常見的處理器都不容許對存在數據依賴的操做進行重排序(對應上面數據轉換那一列,都是N,因此處理器不容許這種重排序)。
那麼這個結論對咱們有什麼做用呢?好比第一點:處理器容許寫-讀操做二者之間的重排序,那麼在併發編程中讀線程讀到多是一個未被初始化或者是一個NULL等,出現不可預知的錯誤,基於這點,JMM會在適當的位置插入內存屏障指令來禁止特定類型的處理器的重排序。內存屏障指令一共有4類:
根據上面的表格,處理器不會對存在數據依賴的操做進行重排序。這裏數據依賴的準肯定義是:若是兩個操做同時訪問一個變量,其中一個操做是寫操做,此時這兩個操做就構成了數據依賴。常見的具備這個特性的如i++、i—。若是改變了具備數據依賴的兩個操做的執行順序,那麼最後的執行結果就會被改變。這也是不能進行重排序的緣由。例如:
a = 1; b = a;
a = 1; a = 2;
a = b; b = 1;
重排序遵照數據依賴性,編譯器和處理器不會改變存在數據依賴關係的兩個操做的執行順序。可是這裏所說的數據依賴性僅針對單個處理器中執行的指令序列和單個線程中執行的操做,不一樣處理器之間和不一樣線程之間的數據依賴性不被編譯器和處理器考慮。
as-if-serial語義的意思指:管怎麼重排序(編譯器和處理器爲了提升並行度),(單線程)程序的執行結果不能被改變。編譯器,runtime 和處理器都必須遵照as-if-serial語義。
as-if-serial語義把單線程程序保護了起來,遵照as-if-serial語義的編譯器,runtime 和處理器共同爲編寫單線程程序的程序員建立了一個幻覺:單線程程序是按程序的順序來執行的。as-if-serial語義使單線程程序員無需擔憂重排序會干擾他們,也無需擔憂內存可見性問題。
若是代碼中存在控制依賴的時候,會影響指令序列執行的並行度(由於高效)。也是爲此,編譯器和處理器會採用猜想(Speculation)執行來克服控制的相關性。因此重排序破壞了程序順序規則(該規則是說指令執行順序與實際代碼的執行順序是一致的,可是處理器和編譯器會進行重排序,只要最後的結果不會改變,該重排序就是合理的)。
在單線程程序中,因爲as-ifserial語義的存在,對存在控制依賴的操做重排序,不會改變執行結果;但在多線程程序中,對存在控制依賴的操做重排序,可能會改變程序的執行結果。
前面所述的內存交互操做必需要知足必定的規則,而happens-before就是定義這些規則的一個等效判斷原則。happens-before是JMM定義的2個操做之間的偏序關係:若是操做A線性發生於操做B,則A產生的影響能被操做B觀察到,「影響」包括修改了內存中共享變量的值、發送了消息、調用了方法等。若是兩個操做知足happens-before原則,那麼不須要進行同步操做,JVM可以保證操做具備順序性,此時不可以隨意的重排序。不然,沒法保證順序性,就能進行指令的重排序。
happens-before原則主要包括:
注意:不一樣操做時間前後順序與先行發生原則之間沒有關係,兩者不能相互推斷,衡量併發安全問題不能受到時間順序的干擾,一切都要以happens-before原則爲準
示例代碼1:歡迎點擊連接加入羣【Java併發編程交流組】:https://jq.qq.com/?_wv=1027&k=5mOvK7L
private int value = 0; public void setValue(int value) { this.value = value; } public int getValue() { return this.value; }
按照happens-before原則,兩個操做不在同一個線程、沒有通道鎖同步、線程的相關啓動、終止和中斷以及對象終結和傳遞性等規則都與此處沒有關係,所以這兩個操做是不符合happens-before原則的,這裏的併發操做是不安全的,返回值並不必定是1。
對於該問題的修復,可使用lock或者synchronized套用「管程鎖定規則」實現先行發生關係;或者將value定義爲volatile變量(兩個方法的調用都不存在數據依賴性),套用「volatile變量規則」實現先行發生關係。如此一來,就能保證併發安全性。
示例代碼2
// 如下操做在同一個線程中 int i = 1; int j = 2;
上面的代碼符合「程序次序規則」,知足先行發生關係,可是第2條語句徹底可能因爲重排序而被處理器先執行,時間上先於第1條語句,歡迎點擊連接加入羣【Java併發編程交流組】:https://jq.qq.com/?_wv=1027&k=5mOvK7L