在併發編程中,咱們須要處理兩個問題,線程之間如何通訊以及線程之間如何同步java
通訊是指線程之間依靠何種機制交換信息。在命令式編程中,線程之間通訊主要依靠共享內存和消息機制來進行通訊。程序員
在共享模型機制中,線程之間共享程序的公共狀態,線程之間經過寫-讀內存中的公共狀態來進行隱式的通訊,在消息傳遞的併發模型裏,線程之間沒有公共狀態,線程之間必須經過明確的消息來進行顯示的通訊。編程
同步是程序用於控制不一樣線程之間操做發生相對順序的機制。在共享內存併發模型裏面,同事是顯示進行的。程序員必須顯示的指定方法或者某段代碼段須要在線程之間互斥執行。在消息傳遞模型中,由於接收消息必須在發送消息以後,因此他們之間的同步是隱式操做數組
java採起的是同步共享併發模型,java線程之間的通訊老是隱式進行,對程序員來講是透明的。緩存
java內存模型:併發
在jvm內存中,定了對象的實例,數組,靜態域是存放在堆內存中的,堆內存這塊就是線程共享的。虛擬機棧、本地方法棧和程序計數器屬於線程私有的,不存在併發的問題,由於不存在內存可見性的問題,也不瘦內存模型的影響。app
java線程通訊是有java內存模型控制,也就是JMM,JMM決定了一個線程對共享變量的寫入操做什麼時候對另外一個線程可見。從抽象的角度講,JMM決定了線程與主內存之間的抽象關係,線程之間的共享變量什麼時候寫入主內存中,每一個線程都存在一個私有的本地內存,本地內存存儲了該線程以讀/寫操做共享變量的副本,這裏所說的本地內存也是一種抽象的概念,它涵蓋了緩存,寫緩衝區,寄存器以及其餘硬件。JAVA內存模型抽象圖以下:jvm
從上圖能夠看出若是線程A要和線程B進行通訊的話,姚經理兩個步驟:高併發
1:首先線程A要把本地變量的副本刷新到主內存中spa
2:而後,線程B去主內存中讀取線程A以前更新的變量
下面是一個示意圖:
如上圖所示,線程A和線程B都有主內存中X變量的副本,線程A在執行的時候,首先把主內存中的變量x load到本地內存中,當線程A須要和線程B進行通訊的時候,線程A再把本地內存中已經更新了的x的值store到主內存中,線程B再從主內存中讀取共享變量x的值到本地內存B之中,而後對線程B來講x的值就會更新爲1。
從總體來看,其實就至關於線程A給線程B發消息,這個通訊過程依靠主內存,JMM控制主內存和本地內存進行交互。
重排序:
在執行程序的時候,編譯器和處理器須要對指令進行重排序。重排序包括三種
1:編譯器的重排序。編譯器在不改變單線程語義的狀況下,能夠安排語句的執行順序,好比
int a = 10; 1
int b = 10; 2
int c = a * b; 3
可能裏面的執行順序就2在一以前,它在保證輸出結果正確的前提下容許指令進行從新排序
2:指令級並行的重排序,現代處理器採用了指令級並行技術來將多條指令並行執行。若是不存在數據依賴,那麼處理器也能夠改變語句對應機器指令的執行順序
3:內存系統重排序,因爲處理器使用緩存進行讀寫操做,這使得加載和存儲看上去是亂序的,從java源代碼到最終執行的二級制代碼,會經歷一下三種排序
第一級別的重排序屬於編譯器級別的重排序,第二和第三級別屬於處理器級別的重排序,對於編譯器,JMM的編譯器重排序規則會禁止特定的編譯重排序(不是全部的編譯器都要重排序),對弈處理器級別的重排序,JMM則要求java編譯器在生成指令序列的時候,插入特定類型的內存屏障指令,經過內存屏障指令來禁止特定類型的處理器重排序(不是全部的處理器重排序)。
JMM屬於語言級別的內存模型,他保證了在不一樣的編譯器和不一樣的處理器平臺上,經過特定類型的編譯器重排序和處理器重排序,爲程序員提供一致的內存可見性保證。
處理器重排序與內存屏障指令
現代的處理器都經過寫緩衝區的方式臨時保存而後在向內存中寫入數據。寫緩衝區的操做保證了指令流水線運行,同事他能夠避免處理器停頓下來等待向內存中寫入數據產生的延遲。同事能夠經過批處理的方式刷新緩衝區,或者合併緩衝區內對同一地址的屢次修改,能夠減小內存總線的佔用。雖然寫緩衝區有不少好處,可是每一個處理器的寫緩衝區只對該處理器可見。這就會對內存的操做順序產生重要的影響:處理器對內存的讀/寫操做的執行順序,不必定與內存實際發生的讀/寫順序一致,爲了具體說明,請看一下實例:
ProcessorA | ProcessorB |
a=1 //A1 | b=2 //B1 |
x=b //A2 | y=a //B2 |
初始狀態是a=b=0
處理器容許執行後獲得的結果是x=y=0
假設程序器A和處理器B按照程序的順序訪問執行內存操做,最終卻可能獲得x=y=0的結果。具體緣由以下:
在這裏處理器A和處理器B同時把共享變量寫入本身的寫緩衝區內,而後從內存中讀取以前的共享變量數據,而後再把本身的寫緩衝區內的數據刷新到內存中,那麼就有可能出現a=b=0的狀況,實際上這種狀況下,處理器A和處理器B讀到的數據就是髒數據。
從內存的執行順序來看,知道處理器A完成了A3,將數據刷新到內存中,纔算是完成了A1這個寫操做,雖然處理器A執行的操做順序A1>A2,可是在內存中卻變成了A2>A1,此時,處理器A的操做順序就被重排序了,處理器B也是同樣
這裏的關鍵地方就是,寫緩衝區僅對本身可見,他會致使處理器執行內存操做的殊勳可能會與內存實際操做順序不一致。因爲如今的處理器都作寫緩衝區,所以如今處理器都會容許對寫-讀操做進行重排序。
下面是常見處理器容許重排序類型的列表:
Load-Load | Load-Store | Store-Store | Store-Load | 數據依賴 | |
sparc-TSO | N | N | N | Y | N |
x86 | N | N | N | Y | N |
ia64 | Y | Y | Y | Y | N |
PowerPC | Y | Y | Y | Y | N |
以上表格N表示不容許,Y表示容許
sparc-TSO是指以TSO(total store order)內存模型運行時,sparc處理器特性
x86包括x64和AMD64
爲了保證內存可見性,java編譯器在生成指令序列的適當位置會插入內存屏障指令來禁止特定類型的處理器重排序。JMM把內存屏障分爲如下四個類型:
屏障類型 | 指令實例 | 說明 |
LoadLoad Barriers | Load1; LoadLoad; Load; |
確保Load1數據的裝載,以前與Load2以及後續全部指令的裝載 |
StoreStore Barriers | Store1; StoreStore; Store2; |
確保Store1的數據對其餘處理器可見(從寫緩衝區刷新到內存), 以前與Store2以及後續全部指令的操做 |
LoadStore Barriers | Load1; LoadStore; Store2; |
確保Load1數據的裝載,以前與Store2以及後續全部指令的操做 |
StoreLoad Barriers | Store1; StoreLoad; Load2; |
確保Store1數據對其餘處理器可見(從寫緩衝區刷新到內存), 以前與Load2及後續全部指令的裝載。 StoreLoad Barries會使該屏障以前的全部內存指令(Store和Load) 完成以後,才執行屏障以後的內存訪問指令。 |
StoreLoad Barries是一個全能型的內存屏障指令,他同時具備其餘三個屏障的效果,現代處理器大都支持該屏障,可是該屏障的開銷會很昂貴,由於當前處理器會把寫緩衝區內的數據所有刷新到內存中去。
happens-before
從jdk1.5開始,java使用最新的JSR-133內存模型。
JSR-133內存模型提出了happens-before的概念,經過這個概念闡述了操做之間的內存可見性。若是一個操做執行結果須要對另外一個操做可見,那麼兩個操做之間必須存在happen-before原則,這裏的兩個操做,既能夠是同一個線程之間的,也能夠是不一樣線程之間的。和咱們平常開發關係密切相關的happens-before原則:
1:程序順序原則:一個線程的每一個操做,happens-before與線程中的任意後續操做
2:監視器鎖規則:對一個監視器鎖的解鎖,happens-before與隨後隊這個監視器鎖的加鎖操做
3:volatile變量規則:對一個volatile域的寫,happens-before與後續對這個volatile域的讀操做
4:傳遞性:若是A操做happens-before與B操做,B操做happens-before與C操做,那麼A操做happens-before與C操做
須要注意的一點就是兩個操做有happens-before規則,可是並不意味着前一個操做必須在後一個操做執行完以後再去執行,這裏happens-before原則僅僅是要求前一個操做的結果必須對後一個操做的結果可見,切前一個程序的操做順序排在第二個以前
好比:
在單線程操做中
double a = 23.32; //A
double b = 23.43; //B
double c = a*b; //C
根據程序的happens-before原則,
A happens-before B
B happens-before C
A happens-before C
那麼是否是就要A必定要在B執行前執行,其實否則,B有可能在A執行前執行。JMM僅僅要求前一個操做的結果要對後一個操做的結果可見,這裏操做A的結果其實不須要對操做B可見,並且重排序操做A和操做B以後的執行結果與A happens-before B順序執行的結果一致,這種狀況下JMM認爲重排序並不非法,JMM容許這樣的重排序。
上述所說的單線程操做其實就是JMM中的as-if-serial語義
as-if-serial語義的意思是指:無論怎麼排序(編譯器和處理器爲了提升併發的速度),(單線程)程序執行結果不會改變,編譯器,runtime,和處理器都必須遵循as-if-serial語義
爲了遵循as-if-serial語義,編譯器和處理器不會對存在有數據依賴關係的的操做進行重排序,由於這種重排序會改變程序的操做結果。可是不存在依賴關係的操做可能會被重排序,上面單線程例子就是。