【java多線程系列】java內存模型與指令重排序

在多線程編程中,須要處理兩個最核心的問題,線程之間如何通訊及線程之間如何同步,線程之間通訊指的是線程之間經過何種機制交換信息,同步指的是如何控制不一樣線程之間操做發生的相對順序。不少讀者可能會說這還不簡單,java中的同步採用的是鎖機制或volatile來完成的,的確,在應用層,java中的同步的確是經過加鎖來完成的,可是鎖機制是如何實現的呢?這就涉及到java中的內存模型的相關知識。本博客將帶領你們瞭解java內存模型的相關知識。java


若是讀者以爲本博客寫的不錯,記得小手一抖,點個贊哦!另外歡迎你們關注個人博客帳號哦,將會不按期的爲你們分享技術乾貨,福利多多哦!程序員


咱們知道java中多線程通訊採用的是共享內存模型,即多個線程之間共享某塊內存,經過寫-讀內存中的公共狀態進行隱式通訊,整個通訊過程對於程序員徹底透明,所以理解java內存模型將幫助咱們理解這種隱式通訊的原理,從而更好的寫出java多線程程序。編程


一java內存模型的抽象結構:數組

咱們知道在java中,對象實例域,靜態域和數組元素存儲在堆內存中,堆內存在線程之間共享,咱們稱對象實例域,靜態域和數組元素爲共享變量,而局部變量,方法定義的參數和異常處理器參數不會在線程之間共享,它們不存在內存可見性的問題,所以不受java內存模型的影響。緩存

java線程之間的通訊受java內存模型(Java Memory Model,簡稱JMM)的控制,JMM決定一個線程對共享變量的寫入什麼時候對另外一個線程可見,從抽象的角度來看,JMM定義了線程和主內存之間的抽象關係:線程之間的共享變量存儲在主內存(Main Memory)中,而每一個線程各自擁有屬於本身的本地內存(Local Memory),本地內存中存儲了該線程以讀/寫共享變量的副本。注意本地內存是一個抽象概念,在物理設備上不存在,它一般包含緩存,寫緩衝區,寄存器以及其餘的硬件和編譯器優化等。java內存模型的抽象示意圖以下:多線程


從圖可知,線程A與線程B之間如要通訊的話,必需要經歷下面2個步驟:性能

1首先,線程A把本地內存A中更新過的共享變量刷新到主內存中去。
2而後,線程B到主內存中去讀取線程A以前已更新過的共享變量。
優化

即java線程之間的通訊必須通過主內存,JMM經過經過控制主內存與每一個線程的本地內存之間的交互,來爲java程序員提供內存可見性保證。spa


二指令序列的重排序:線程

前面說過每一個線程擁有本身的本地內存(一個抽象的概念,多個物理設備內存的抽象),其中一種就是硬件和編譯器優化。在執行程序時,爲了提升性能,編譯器和處理器一般會對指令作重排序,之因此把這個拿出來說,是由於咱們知道CPU將按照指令序列執行指令,若是指令被重排序,那麼對線程的讀寫會產生影響,這就會影響咱們前面提到的java內存模型。因此接下來就介紹一下重排序,重排序包括3種類型

1)編譯器優化的重排序。編譯器在不改變單線程程序語義的前提下,能夠從新安排語句的執行順序。
2)指令級並行的重排序。現代處理器採用了指令級並行技術(Instruction-Level Parallelism, ILP)來將多條指令重疊執行。若是不存在數據依賴性,處理器能夠改變語句對          應機器指令的執行順序。
3)內存系統的重排序。因爲處理器使用緩存和讀/寫緩衝區,這使得加載和存儲操做看上去多是在亂序執行

其中第一種很好理解,這是保證程序順序執行最基本的原則,第二條中的若是數據不存在依賴關係這點給你們解釋一下,示例代碼以下:

int x=1;
int y=x+1;
int z=1;

由於第二行語句中y=x+1,即y的結果依賴於x的值,那麼y與x存在依賴關係,而z與x與y不存在依賴關係,因此在指令重排序後x必須始終在y的前面出現,而z與x與y之間的關係能夠亂序,即重排序後結果能夠爲:

int z=1;
int x=1;
int y=x+1;
但不能爲:

int z=1;
int y=x+1;
int x=1;//x賦值必須在y以前
上述3種重排序可能會致使多線程程序出現內存可見性問題,對於編譯器,JMM的編譯器重排序規則會禁止特定類型的編譯器重排序,對於處理器,JMM的處理器重排序規則會要求java編譯器在生成指令序列時,插入特定類型的內存屏障(memory barriers,intel稱之爲memory fence)指令,經過內存屏障指令來禁止特定類型的處理器重排序



三java內存模型內存屏障指令

前面說過,常見的處理器都會對程序指令進行重排序,而這在多線程中極可能致使內存可見性問題,而java內存模型確保在不一樣的編譯器和不一樣的處理器平臺之上,經過禁止特定類型的編譯器重排序和處理器重排序,爲程序員提供一致的內存可見性保證。這也是java內存模型根本做用。而禁止重排序的方法就是插入內存屏障指令,爲了更好的理解爲什麼須要禁止重排序,咱們先來看一個例子:


假設處理器A和處理器B按程序的順序並行執行內存訪問,最終卻可能獲得x = y = 0的結果。具體的緣由以下圖所示:


這裏對這個圖稍做一下解釋,由於寫緩衝區僅對本身的處理器可見,因此雖然處理器A已經在緩衝區A中更新了a的值,可是處理器B不能感知到,所以處理器B從內存中讀取a的值賦給y時,若是此時處理器A還未將a的值刷新到內存中,那麼此時內存中a的值仍然爲0,這樣y的值就爲0,同理x的值可能爲0,而這顯然不是咱們所指望的結果,


之因此出現上述結果是由於現代的處理器都會使用寫緩衝區來臨時保存向內存寫入的數據,這相信你們在計算機組成原理這麼課中都學過,但每一個處理器上的寫緩衝區,僅僅對它所在的處理器可見。這個特性會對內存操做的執行順序產生重要的影響:處理器對內存的讀/寫操做的執行順序,不必定與內存實際發生的讀/寫操做順序一致

咱們知道對內存的操做包括讀-寫兩種,那麼多線程訪問同一個共享變量則兩兩組合共四種狀況,現代常見處理器的重排序對這四種組合容許狀況以下所示:


上圖中「N」表示處理器不容許兩個操做重排序,「Y」表示容許重排序。咱們能夠看出:常見的處理器都容許Store-Load重排序;常見的處理器都不容許對存在數據依賴的操做作重排序。(注意上圖所說的x86包括x64及AMD64。)

與上圖對應java內存模型定義了四種禁止重排序的四種指令屏障,以下圖所示:


java內存模型經過這四種內存屏障指令來保證了前面咱們所舉的例子的狀況不會出現,仍然以上述例子來講明,Java內存模型經過在適當位置插入內存屏障指令,如StoreLoad Barriers指令,則能夠保證Store1數據對其餘處理器是可見的(即將緩存中的內容刷新到內存),這樣在處理器A將a的值=1寫入緩衝區A後將及時保證處理器B在從內存中讀取a的值以前會將處理器A緩存中的值刷新到內存中。從而保證內存可見性。


注意:StoreLoad Barriers是一個「全能型」的屏障,它同時具備其餘三個屏障的效果。現代的多處理器大都支持該屏障(其餘類型的屏障不必定被全部處理器支持)。執行該屏障開銷會很昂貴,由於當前處理器一般要把寫緩衝區中的數據所有刷新到內存中(buffer fully flush)。


以上就是本博客的主要內容,java內存模型主要解決多線程程序中的內存可見性問題,該內容是理解java多線程編程的理論基礎。



若是讀者以爲本博客寫的不錯,記得小手一抖,點個贊哦!另外歡迎你們關注個人博客帳號哦,將會不按期的爲你們分享技術乾貨,福利多多哦!
相關文章
相關標籤/搜索