資料競爭與順序一致性保證
當程式未正確同步時,就會存在資料競爭。 java記憶體模型規格對資料競爭的定義如下:
在一個執行緒中寫一個變量,
在另一個執行緒讀同一個變量,
而且寫和讀取沒有透過同步來排序。
當程式碼包含資料競爭時,程式的執行往往產生違反直覺的結果(前一章的範例正是如此)。如果一個多執行緒程式能正確同步,這個程式將會是一個沒有資料競爭的程式。
JMM對正確同步的多執行緒程式的記憶體一致性做了以下保證:
如果程式是正確同步的,程式的執行將具有順序一致性(sequentially consistent)–即程式的執行結果與該程式在順序一致性記憶體模型中的執行結果相同(馬上我們將會看到,這對程式設計師來說是一個極強的保證)。這裡的同步是指廣義的同步,包括對常用同步原語(lock,volatile和final)的正確使用。
順序一致性記憶體模型
順序一致性記憶體模型是一個被電腦科學家理想化了的理論參考模型,它為程式設計師提供了極強的記憶體可見性保證。順序一致性記憶體模型有兩大特性:
一個執行緒中的所有操作必須按照程式的順序來執行。
(不管程式是否同步)所有執行緒都只能看到一個單一的操作執行順序。在順序一致性記憶體模型中,每個操作都必須原子執行且立刻對所有執行緒可見。
順序一致性內存模型為程式設計師提供的視圖如下:
在概念上,順序一致性模型有一個單一的全局內存,這個內存通過一個左右擺動的開關可以連接到任意一個線程。同時,每一個執行緒必須按程式的順序來執行記憶體讀取/寫入操作。從上圖我們可以看出,在任意時間點最多只能有一個執行緒可以連接到記憶體。當多個執行緒並發執行時,圖中的開關裝置能把所有執行緒的所有記憶體讀/寫操作串行化。
為了更好的理解,以下我們透過兩個示意圖來對順序一致性模型的特性做進一步的說明。
假設有兩個執行緒A和B並發執行。其中A執行緒有三個操作,它們在程式中的順序是:A1->A2->A3。 B執行緒也有三個操作,它們在程式中的順序是:B1->B2->B3。
假設這兩個執行緒使用監視器來正確同步:A執行緒的三個操作執行後釋放監視器,隨後B執行緒取得同一個監視器。那麼程式在順序一致性模型中的執行效果將如下圖:
現在我們再假設這兩個執行緒沒有做同步,以下是這個未同步程式在順序一致性模型中的執行示意圖:
未同步程序在順序一致性模型中雖然整體執行順序是無序的,但所有執行緒都只能看到一個一致的整體執行順序。以上圖為例,線程A和B看到的執行順序都是:B1->A1->A2->B2->A3->B3。之所以能得到這個保證是因為順序一致性記憶體模型中的每個操作必須立即對任意執行緒可見。
但是,在JMM中就沒有這個保證。未同步程序在JMM中不但整體的執行順序是無序的,而且所有執行緒看到的操作執行順序也可能不一致。例如,在當前線程把寫過的資料緩存在本地記憶體中,而且還沒有刷新到主內存之前,這個寫操作僅對當前線程可見;從其他線程的角度來觀察,會認為這個寫操作根本還沒有被當前線程執行。只有當前線程把本地記憶體中寫過的資料刷新到主記憶體之後,這個寫入操作才能對其他線程可見。在這種情況下,當前執行緒和其它執行緒看到的操作執行順序將不一致。
同步程式的順序一致性效果
下面我們對前面的範例程式ReorderExample用監視器來同步,看看正確同步的程式如何具有順序一致性。
請看下面的範例程式碼:
class SynchronizedExample { int a = 0; boolean flag = false; public synchronized void writer() { a = 1; flag = true; } public synchronized void reader() { if (flag) { int i = a; …… } } }
上面範例程式碼中,假設A執行緒執行writer()方法後,B執行緒執行reader()方法。這是一個正確同步的多執行緒程式。根據JMM規範,該程序的執行結果將與該程序在順序一致性模型中的執行結果相同。以下是程式在兩個記憶體模型中的執行時序比較圖:
在順序一致性模型中,所有操作完全依照程式的順序串列執行。而在JMM中,臨界區內的程式碼可以重新排序(但JMM不允許臨界區內的程式碼「逸出」到臨界區之外,那樣會破壞監視器的語意)。 JMM會在退出監視器和進入監視器這兩個關鍵時間點做一些特別處理,使得線程在這兩個時間點具有與順序一致性模型相同的內存視圖(具體細節後文會說明)。雖然線程A在臨界區內做了重排序,但由於監視器的互斥執行的特性,這裡的線程B根本無法「觀察」到線程A在臨界區內的重排序。這種重排序既提高了執行效率,又沒有改變程式的執行結果。
從這裡我們可以看到JMM在具體實現上的基本方針:在不改變(正確同步的)程序執行結果的前提下,盡可能的為編譯器和處理器的優化打開方便之門。
未同步程序的執行特性
對於未同步或未正確同步的多執行緒程序,JMM只提供最小安全性:執行緒執行時讀取到的值,要不是之前某個執行緒寫入的值,就是是預設值(0,null,false),JMM保證執行緒讀取操作讀取到的值不會無中生有(out of thin air)的冒出來。為了實現最小安全性,JVM在堆上分配物件時,首先會清除記憶體空間,然後才會在上面分配物件(JVM內部會同步這兩個操作)。因此,在以清零的記憶體空間(pre-zeroed memory)分配物件時,域的預設初始化已經完成了。
JMM不保證未同步程序的執行結果與該程序在順序一致性模型中的執行結果一致。因為未同步程序在順序一致性模型中執行時,整體上是無序的,其執行結果無法預知。保證未同步程序在兩個模型中的執行結果一致毫無意義。
和順序一致性模型一樣,未同步程序在JMM中的執行時,整體上也是無序的,其執行結果也無法預知。同時,未同步程序在這兩個模型中的執行特性有以下幾個差異:
順序一致性模型保證單執行緒內的操作會依照程式的順序執行,而JMM不保證單執行緒內的操作會依照程序的順序執行(例如上面正確同步的多執行緒程式在臨界區內的重排序)。這一點前面已經講過了,這裡就不再贅述。
順序一致性模型保證所有執行緒只能看到一致的操作執行順序,而JMM不保證所有執行緒能看到一致的操作執行順序。這一點前面也已經講過,這裡就不再贅述。
JMM不保證對64位的long型和double型變數的讀/寫操作具有原子性,而順序一致性模型保證對所有的記憶體讀/寫操作都具有原子性。
第3個差異與處理器匯流排的工作機制密切相關。在計算機中,資料透過匯流排在處理器和記憶體之間傳遞。每次處理器和記憶體之間的資料傳遞都是透過一系列步驟來完成的,這一系列步驟稱之為總線事務(bus transaction)。總線事務包括讀取事務(read transaction)和寫入事務(write transaction)。讀取交易從內存傳送資料到處理器,寫事務從處理器傳送資料到內存,每個事務會讀/寫內存中一個或多個物理上連續的字。這裡的關鍵是,匯流排會同步試圖並發使用匯流排的事務。在一個處理器執行匯流排事務期間,匯流排會禁止其它所有的處理器和I/O裝置執行記憶體的讀取/寫入。下面讓我們透過一個示意圖來說明匯流排的工作機制:
如上圖所示,假設處理器A,B和C同時向總線發起總線事務,這時總線仲裁(bus arbitration)會對競爭作出裁決,這裡我們假設總線在仲裁後判定處理器A在競爭中獲勝(匯流排仲裁會確保所有處理器都能公平的存取記憶體)。此時處理器A繼續它的匯流排事務,而其它兩個處理器則要等待處理器A的匯流排事務完成後才能開始再次執行記憶體存取。假設在處理器A執行匯流排事務期間(不管這個匯流排事務是讀事務還是寫事務),處理器D向匯流排發起了匯流排事務,此時處理器D的這個請求會被匯流排禁止。
匯流排的這些工作機制可以把所有處理器對記憶體的存取以串列化的方式來執行;在任意時間點,最多只能有一個處理器能存取記憶體。這個特性確保了單一匯流排事務之中的記憶體讀/寫操作具有原子性。
在一些32位元的處理器上,如果要求對64位元資料的讀取/寫入操作具有原子性,會有比較大的開銷。為了照顧這個處理器,java語言規格鼓勵但不強求JVM對64位元的long型變數和double型變數的讀/寫具有原子性。當JVM在這種處理器上運作時,會把一個64位元long/ double型變數的讀/寫操作拆分為兩個32位元的讀/寫操作來執行。這兩個32位元的讀/寫操作可能會被分配到不同的匯流排事務中執行,此時對這個64位元變數的讀/寫將不具有原子性。
當單一記憶體操作不具原子性,將可能會產生意想不到後果。請看下面示意圖:
如上圖所示,假設處理器A寫一個long型變量,同時處理器B要讀這個long型變數。處理器A中64位元的寫入操作被分割為兩個32位元的寫入操作,且這兩個32位元的寫入操作被分配到不同的寫入事務中執行。同時處理器B中64位元的讀取操作被分割為兩個32位元的讀取操作,且這兩個32位元的讀取操作被分配到同一個的讀取事務中執行。當處理器A和B按上圖的時序來執行時,處理器B將看到僅被處理器A「寫了一半「的無效值。
以上就是Java記憶體模型深度解析:順序一致性的內容,更多相關內容請關注PHP中文網(www.php.cn)!