深入理解java記憶體模型03 順序一致性

2021-08-21 06:05:02 字數 2319 閱讀 6690

程式中用到了區域性變數 r1 和 r2,以及共享變數 a 和 b。可能會出現 r2 == 2、 r1 == 1 這樣的結果。

直覺上,應當要麼指令 1 先執行要麼指令 3先執行。

然而,從單個執行緒的角度看,只要重排序不會影響到該執行緒的執行結果,編譯器就可以對該執行緒中的指令進行重排序如果指令 1 與指令 2 重排序,那就很容易看出為什麼會出現 r2 == 2 和 r1 == 1 這樣的結果了。

一些程式設計人員可能認為程式表現出這種行為是不對的。但是,需要注意的是,這段**沒有被充分同步

當上述情況發生時,稱之為存在資料爭用(data race)。當**中存在資料爭用時,常有可能出現有違直覺的結果。如果乙個多執行緒程式能正確的同步,則它是乙個沒有資料競爭的程式,常用的同步原語如synchronized,volitale和final。

為了更好的理解,下面我們通過兩個示意圖來對順序一致性模型的特性做進一步的說明。

假設有兩個執行緒a和b併發執行。其中a執行緒有三個操作,它們在程式中的順序是:a1->a2->a3。b執行緒也有三個操作,它們在程式中的順序是:b1->b2->b3。

未同步程式在順序一致性模型中雖然整體執行順序是無序的,但所有執行緒都只能看到乙個一致的整體執行順序。以上圖為例,執行緒a和b看到的執行順序都是:b1->a1->a2->b2->a3->b3。之所以能得到這個保證是因為順序一致性記憶體模型中的每個操作必須立即對任意執行緒可見。

但是在jmm中就沒有這個保證。未同步程式在jmm中不但整體的執行順序是無序的,而且所有執行緒看到的操作執行順序也可能不一致(非原子操作,指令重排)。

public

class

reorderexample

public

synchronized

void

reader() }}

上面示例**中,假設a執行緒執行writer()方法後,b執行緒執行reader()方法。這是乙個正確同步的多執行緒程式。根據jmm規範,該程式的執行結果將與該程式在順序一致性模型中的執行結果相同。下面是該程式在兩個記憶體模型中的執行時序對比圖:

在順序一致性模型中,所有操作完全按照程式的順序序列執行。而在jmm中,臨界區內的**可以重排序。(但jmm不允許臨界區內的**「逸出」到臨界區之外,那樣會破壞監視器的語義)。jmm在退出臨界區和進入臨界區這兩個關鍵時間點會做一些特別處理(當執行緒釋放鎖時,jmm會把該執行緒對應的本地記憶體中的共享變數重新整理到主記憶體中; 當執行緒獲取鎖時,jmm會把該執行緒對應的本地記憶體置為無效。),使得執行緒在這兩個時間點具有與順序一致性模型相同的記憶體檢視。雖然執行緒a在臨界區內做了重排序,但由於監視器的互斥執行的特性,這裡的執行緒b根本無法「觀察」到執行緒a在臨界區內的重排序。這種重排序既提高了執行效率,又沒有改變程式的執行結果。

從這裡我們可以看到jmm在具體實現上的基本方針:在不改變(正確同步的)程式執行結果的前提下,盡可能的為編譯器和處理器的優化開啟方便之門。

對於未同步或未正確同步的多執行緒程式,jmm只提供最小安全性:執行緒執行時讀取到的值,要麼是之前某個執行緒寫入的值,要麼是預設值(0,null,false),jmm保證執行緒讀操作讀取到的值不會無中生有(out of thin air)的冒出來。為了實現最小安全性,jvm在堆上分配物件時,首先會清零記憶體空間,然後才會在上面分配物件(jvm內部會同步這兩個操作)。因此,在以清零的記憶體空間(pre-zeroed memory)分配物件時,域的預設初始化已經完成了。

jmm不保證未同步程式的執行結果與該程式在順序一致性模型中的執行結果一致。因為未同步程式在順序一致性模型中執行時,整體上是無序的,其執行結果無法預知。未同步程式在順序一致性模型和jmm有如下差異:

對於64位的資料型別(long,double),jmm特別定義一條寬鬆的規定:允許虛擬機器將沒有被volatile修飾的64位資料的讀寫操作拆分為兩次32位操作來進行。即允許虛擬機器實現可以不保證64位資料型別的load,store,read和write 4個操作的原子性,這就是long和double的費原子協定(nonatomic treatment of long and double variables).

在實際開發中,目前各平台下的商用虛擬機器幾乎都選擇把64位資料的讀寫操作作為原子操作來對待,因此在編寫**的時候一般不需要把long和double變數宣告為volitale.

深入理解Java記憶體模型(二) 重排序

如果兩個操作訪問同乙個變數,且這兩個操作中有乙個為寫操作,此時這兩個操作之間就存在資料依賴性。資料依賴分下列三種型別 上面三種情況,只要重排序兩個操作的執行順序,程式的執行結果將會被改變。前面提到過,編譯器和處理器可能會對操作做重排序。編譯器和處理器在重排序時,會遵守資料依賴性,編譯器和處理器不會改...

深入理解java記憶體模型(二)重排序

資料依賴性 如果兩個操作訪問同乙個變數,且這兩個操作中有乙個為寫操作,此時這兩個操作之間就存在資料依賴性。資料依賴分下列三種型別 名稱 示例 說明 寫後讀 a 1 b a 寫乙個變數之後,再讀這個位置。寫後寫 a 1 a 2 寫乙個變數之後,再寫這個變數。讀後寫 a b b 1 讀乙個變數之後,再寫...

深入理解Java記憶體模型二 重排序

如果兩個操作訪問同乙個變數,且這兩個操作中有乙個為寫操作,此時這兩個操作之間就存在資料依賴性。資料依賴分下列三種型別 上面三種情況,只要重排序兩個操作的執行順序,程式的執行結果將會被改變。前面提到過,編譯器和處理器可能會對操作做重排序。編譯器和處理器在重排序時,會遵守資料依賴性,編譯器和處理器不會改...