深刻理解Java內存模型(三)——順序一致性

做者 程曉明 發佈於 2013年2月5日

領域 架構 & 設計語言 & 開發 主題 Java , 多線程 , 併發 , 內存模型 , 專欄 html

數據競爭與順序一致性保證

當程序未正確同步時,就會存在數據競爭。java內存模型規範對數據競爭的定義以下: java

  • 在一個線程中寫一個變量,
  • 在另外一個線程讀同一個變量,
  • 並且寫和讀沒有經過同步來排序。

當代碼中包含數據競爭時,程序的執行每每產生違反直覺的結果(前一章的示例正是如此)。若是一個多線程程序能正確同步,這個程序將是一個沒有數據競爭的程序。 程序員

JMM對正確同步的多線程程序的內存一致性作了以下保證: 編程

  • 若是程序是正確同步的,程序的執行將具備順序一致性(sequentially consistent)--即程序的執行結果與該程序在順序一致性內存模型中的執行結果相同(立刻咱們將會看到,這對於程序員來講是一個極強的保證)。這裏的同步是指廣義上的同步,包括對經常使用同步原語(lock,volatile和final)的正確使用。

順序一致性內存模型

順序一致性內存模型是一個被計算機科學家理想化了的理論參考模型,它爲程序員提供了極強的內存可見性保證。順序一致性內存模型有兩大特性: 緩存

  • 一個線程中的全部操做必須按照程序的順序來執行。
  • (無論程序是否同步)全部線程都只能看到一個單一的操做執行順序。在順序一致性內存模型中,每一個操做都必須原子執行且馬上對全部線程可見。

順序一致性內存模型爲程序員提供的視圖以下: 安全

在概念上,順序一致性模型有一個單一的全局內存,這個內存經過一個左右擺動的開關能夠鏈接到任意一個線程。同時,每個線程必須按程序的順序來執行內存讀/寫操做。從上圖咱們能夠看出,在任意時間點最多隻能有一個線程能夠鏈接到內存。當多個線程併發執行時,圖中的開關裝置能把全部線程的全部內存讀/寫操做串行化。 多線程

爲了更好的理解,下面咱們經過兩個示意圖來對順序一致性模型的特性作進一步的說明。 架構

假設有兩個線程A和B併發執行。其中A線程有三個操做,它們在程序中的順序是:A1->A2->A3。B線程也有三個操做,它們在程序中的順序是:B1->B2->B3。 併發

假設這兩個線程使用監視器來正確同步:A線程的三個操做執行後釋放監視器,隨後B線程獲取同一個監視器。那麼程序在順序一致性模型中的執行效果將以下圖所示: oracle

如今咱們再假設這兩個線程沒有作同步,下面是這個未同步程序在順序一致性模型中的執行示意圖:

未同步程序在順序一致性模型中雖然總體執行順序是無序的,但全部線程都只能看到一個一致的總體執行順序。以上圖爲例,線程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中的執行時,總體上也是無序的,其執行結果也沒法預知。同時,未同步程序在這兩個模型中的執行特性有下面幾個差別:

  1. 順序一致性模型保證單線程內的操做會按程序的順序執行,而JMM不保證單線程內的操做會按程序的順序執行(好比上面正確同步的多線程程序在臨界區內的重排序)。這一點前面已經講過了,這裏就再也不贅述。
  2. 順序一致性模型保證全部線程只能看到一致的操做執行順序,而JMM不保證全部線程能看到一致的操做執行順序。這一點前面也已經講過,這裏就再也不贅述。
  3. 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「寫了一半「的無效值。

參考文獻

  1. JSR-133: Java Memory Model and Thread Specification
  2. Shared memory consistency models: A tutorial
  3. The JSR-133 Cookbook for Compiler Writers
  4. 深刻理解計算機系統(原書第2版)
  5. UNIX Systems for Modern Architectures: Symmetric Multiprocessing and Caching for Kernel Programmers
  6. The Java Language Specification, Third Edition

做者簡介

程曉明,Java軟件工程師,國家認證的系統分析師、信息項目管理師。專一於併發編程,就任於富士通南大。我的郵箱:asst2003@163.com

相關文章
相關標籤/搜索