鍍金池/ 教程/ Java/ 順序一致性
final
volatile
重排序
總結(jié)
基礎(chǔ)
順序一致性

順序一致性

數(shù)據(jù)競爭與順序一致性保證

當(dāng)程序未正確同步時(shí),就會(huì)存在數(shù)據(jù)競爭。java 內(nèi)存模型規(guī)范對數(shù)據(jù)競爭的定義如下:

  • 在一個(gè)線程中寫一個(gè)變量,
  • 在另一個(gè)線程讀同一個(gè)變量,
  • 而且寫和讀沒有通過同步來排序。

當(dāng)代碼中包含數(shù)據(jù)競爭時(shí),程序的執(zhí)行往往產(chǎn)生違反直覺的結(jié)果(前一章的示例正是如此)。如果一個(gè)多線程程序能正確同步,這個(gè)程序?qū)⑹且粋€(gè)沒有數(shù)據(jù)競爭的程序。

JMM 對正確同步的多線程程序的內(nèi)存一致性做了如下保證:

  • 如果程序是正確同步的,程序的執(zhí)行將具有順序一致性(sequentially consistent)–即程序的執(zhí)行結(jié)果與該程序在順序一致性內(nèi)存模型中的執(zhí)行結(jié)果相同(馬上我們將會(huì)看到,這對于程序員來說是一個(gè)極強(qiáng)的保證)。這里的同步是指廣義上的同步,包括對常用同步原語(lock,volatile 和 final)的正確使用。

順序一致性內(nèi)存模型

順序一致性內(nèi)存模型是一個(gè)被計(jì)算機(jī)科學(xué)家理想化了的理論參考模型,它為程序員提供了極強(qiáng)的內(nèi)存可見性保證。順序一致性內(nèi)存模型有兩大特性:

  • 一個(gè)線程中的所有操作必須按照程序的順序來執(zhí)行。
  • (不管程序是否同步)所有線程都只能看到一個(gè)單一的操作執(zhí)行順序。在順序一致性內(nèi)存模型中,每個(gè)操作都必須原子執(zhí)行且立刻對所有線程可見。

順序一致性內(nèi)存模型為程序員提供的視圖如下:

http://wiki.jikexueyuan.com/project/java-memory-model/images/10.png" alt="" />

在概念上,順序一致性模型有一個(gè)單一的全局內(nèi)存,這個(gè)內(nèi)存通過一個(gè)左右擺動(dòng)的開關(guān)可以連接到任意一個(gè)線程。同時(shí),每一個(gè)線程必須按程序的順序來執(zhí)行內(nèi)存讀/寫操作。從上圖我們可以看出,在任意時(shí)間點(diǎn)最多只能有一個(gè)線程可以連接到內(nèi)存。當(dāng)多個(gè)線程并發(fā)執(zhí)行時(shí),圖中的開關(guān)裝置能把所有線程的所有內(nèi)存讀/寫操作串行化。

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

假設(shè)有兩個(gè)線程A和B并發(fā)執(zhí)行。其中 A 線程有三個(gè)操作,它們在程序中的順序是:A1->A2->A3。B線程也有三個(gè)操作,它們在程序中的順序是:B1->B2->B3。

假設(shè)這兩個(gè)線程使用監(jiān)視器來正確同步:A 線程的三個(gè)操作執(zhí)行后釋放監(jiān)視器,隨后 B 線程獲取同一個(gè)監(jiān)視器。那么程序在順序一致性模型中的執(zhí)行效果將如下圖所示:

http://wiki.jikexueyuan.com/project/java-memory-model/images/11.png" alt="" />

現(xiàn)在我們再假設(shè)這兩個(gè)線程沒有做同步,下面是這個(gè)未同步程序在順序一致性模型中的執(zhí)行示意圖:

http://wiki.jikexueyuan.com/project/java-memory-model/images/12.png" alt="" />

未同步程序在順序一致性模型中雖然整體執(zhí)行順序是無序的,但所有線程都只能看到一個(gè)一致的整體執(zhí)行順序。以上圖為例,線程 A 和 B 看到的執(zhí)行順序都是:B1->A1->A2->B2->A3->B3。之所以能得到這個(gè)保證是因?yàn)轫樞蛞恢滦詢?nèi)存模型中的每個(gè)操作必須立即對任意線程可見。

但是,在 JMM 中就沒有這個(gè)保證。未同步程序在 JMM 中不但整體的執(zhí)行順序是無序的,而且所有線程看到的操作執(zhí)行順序也可能不一致。比如,在當(dāng)前線程把寫過的數(shù)據(jù)緩存在本地內(nèi)存中,且還沒有刷新到主內(nèi)存之前,這個(gè)寫操作僅對當(dāng)前線程可見;從其他線程的角度來觀察,會(huì)認(rèn)為這個(gè)寫操作根本還沒有被當(dāng)前線程執(zhí)行。只有當(dāng)前線程把本地內(nèi)存中寫過的數(shù)據(jù)刷新到主內(nèi)存之后,這個(gè)寫操作才能對其他線程可見。在這種情況下,當(dāng)前線程和其它線程看到的操作執(zhí)行順序?qū)⒉灰恢隆?/p>

同步程序的順序一致性效果

下面我們對前面的示例程序 ReorderExample 用監(jiān)視器來同步,看看正確同步的程序如何具有順序一致性。

請看下面的示例代碼:

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;
        ……
    }
}
}  

上面示例代碼中,假設(shè) A 線程執(zhí)行 writer() 方法后,B 線程執(zhí)行 reader() 方法。這是一個(gè)正確同步的多線程程序。根據(jù) JMM 規(guī)范,該程序的執(zhí)行結(jié)果將與該程序在順序一致性模型中的執(zhí)行結(jié)果相同。下面是該程序在兩個(gè)內(nèi)存模型中的執(zhí)行時(shí)序?qū)Ρ葓D:

http://wiki.jikexueyuan.com/project/java-memory-model/images/13.png" alt="" />

在順序一致性模型中,所有操作完全按程序的順序串行執(zhí)行。而在 JMM 中,臨界區(qū)內(nèi)的代碼可以重排序(但 JMM 不允許臨界區(qū)內(nèi)的代碼“逸出”到臨界區(qū)之外,那樣會(huì)破壞監(jiān)視器的語義)。JMM會(huì)在退出監(jiān)視器和進(jìn)入監(jiān)視器這兩個(gè)關(guān)鍵時(shí)間點(diǎn)做一些特別處理,使得線程在這兩個(gè)時(shí)間點(diǎn)具有與順序一致性模型相同的內(nèi)存視圖(具體細(xì)節(jié)后文會(huì)說明)。雖然線程 A 在臨界區(qū)內(nèi)做了重排序,但由于監(jiān)視器的互斥執(zhí)行的特性,這里的線程 B 根本無法“觀察”到線程 A 在臨界區(qū)內(nèi)的重排序。這種重排序既提高了執(zhí)行效率,又沒有改變程序的執(zhí)行結(jié)果。

從這里我們可以看到 JMM 在具體實(shí)現(xiàn)上的基本方針:在不改變(正確同步的)程序執(zhí)行結(jié)果的前提下,盡可能的為編譯器和處理器的優(yōu)化打開方便之門。

未同步程序的執(zhí)行特性

對于未同步或未正確同步的多線程程序,JMM 只提供最小安全性:線程執(zhí)行時(shí)讀取到的值,要么是之前某個(gè)線程寫入的值,要么是默認(rèn)值(0,null,false),JMM 保證線程讀操作讀取到的值不會(huì)無中生有(out of thin air)的冒出來。為了實(shí)現(xiàn)最小安全性,JVM 在堆上分配對象時(shí),首先會(huì)清零內(nèi)存空間,然后才會(huì)在上面分配對象(JVM內(nèi)部會(huì)同步這兩個(gè)操作)。因此,在以清零的內(nèi)存空間(pre-zeroed memory)分配對象時(shí),域的默認(rèn)初始化已經(jīng)完成了。

JMM 不保證未同步程序的執(zhí)行結(jié)果與該程序在順序一致性模型中的執(zhí)行結(jié)果一致。因?yàn)槲赐匠绦蛟陧樞蛞恢滦阅P椭袌?zhí)行時(shí),整體上是無序的,其執(zhí)行結(jié)果無法預(yù)知。保證未同步程序在兩個(gè)模型中的執(zhí)行結(jié)果一致毫無意義。

和順序一致性模型一樣,未同步程序在 JMM 中的執(zhí)行時(shí),整體上也是無序的,其執(zhí)行結(jié)果也無法預(yù)知。同時(shí),未同步程序在這兩個(gè)模型中的執(zhí)行特性有下面幾個(gè)差異:

  1. 順序一致性模型保證單線程內(nèi)的操作會(huì)按程序的順序執(zhí)行,而 JMM 不保證單線程內(nèi)的操作會(huì)按程序的順序執(zhí)行(比如上面正確同步的多線程程序在臨界區(qū)內(nèi)的重排序)。這一點(diǎn)前面已經(jīng)講過了,這里就不再贅述。
  2. 順序一致性模型保證所有線程只能看到一致的操作執(zhí)行順序,而JMM不保證所有線程能看到一致的操作執(zhí)行順序。這一點(diǎn)前面也已經(jīng)講過,這里就不再贅述。
  3. JMM不保證對64位的 long 型和 double 型變量的讀/寫操作具有原子性,而順序一致性模型保證對所有的內(nèi)存讀/寫操作都具有原子性。

第3個(gè)差異與處理器總線的工作機(jī)制密切相關(guān)。在計(jì)算機(jī)中,數(shù)據(jù)通過總線在處理器和內(nèi)存之間傳遞。每次處理器和內(nèi)存之間的數(shù)據(jù)傳遞都是通過一系列步驟來完成的,這一系列步驟稱之為總線事務(wù)(bus transaction)??偩€事務(wù)包括讀事務(wù)(read transaction)和寫事務(wù)(write transaction)。讀事務(wù)從內(nèi)存?zhèn)魉蛿?shù)據(jù)到處理器,寫事務(wù)從處理器傳送數(shù)據(jù)到內(nèi)存,每個(gè)事務(wù)會(huì)讀/寫內(nèi)存中一個(gè)或多個(gè)物理上連續(xù)的字。這里的關(guān)鍵是,總線會(huì)同步試圖并發(fā)使用總線的事務(wù)。在一個(gè)處理器執(zhí)行總線事務(wù)期間,總線會(huì)禁止其它所有的處理器和 I/O 設(shè)備執(zhí)行內(nèi)存的讀/寫。下面讓我們通過一個(gè)示意圖來說明總線的工作機(jī)制:

http://wiki.jikexueyuan.com/project/java-memory-model/images/14.png" alt="" />

如上圖所示,假設(shè)處理器 A,B 和 C 同時(shí)向總線發(fā)起總線事務(wù),這時(shí)總線仲裁(bus arbitration)會(huì)對競爭作出裁決,這里我們假設(shè)總線在仲裁后判定處理器A在競爭中獲勝(總線仲裁會(huì)確保所有處理器都能公平的訪問內(nèi)存)。此時(shí)處理器 A 繼續(xù)它的總線事務(wù),而其它兩個(gè)處理器則要等待處理器A的總線事務(wù)完成后才能開始再次執(zhí)行內(nèi)存訪問。假設(shè)在處理器 A 執(zhí)行總線事務(wù)期間(不管這個(gè)總線事務(wù)是讀事務(wù)還是寫事務(wù)),處理器D向總線發(fā)起了總線事務(wù),此時(shí)處理器 D 的這個(gè)請求會(huì)被總線禁止。

總線的這些工作機(jī)制可以把所有處理器對內(nèi)存的訪問以串行化的方式來執(zhí)行;在任意時(shí)間點(diǎn),最多只能有一個(gè)處理器能訪問內(nèi)存。這個(gè)特性確保了單個(gè)總線事務(wù)之中的內(nèi)存讀/寫操作具有原子性。

在一些32位的處理器上,如果要求對64位數(shù)據(jù)的寫操作具有原子性,會(huì)有比較大的開銷。為了照顧這種處理器,java 語言規(guī)范鼓勵(lì)但不強(qiáng)求 JVM 對64位的 long 型變量和 double 型變量的寫具有原子性。當(dāng) JVM 在這種處理器上運(yùn)行時(shí),會(huì)把一個(gè)64位 long/ double 型變量的寫操作拆分為兩個(gè)32位的寫操作來執(zhí)行。這兩個(gè)32位的寫操作可能會(huì)被分配到不同的總線事務(wù)中執(zhí)行,此時(shí)對這個(gè)64位變量的寫將不具有原子性。

當(dāng)單個(gè)內(nèi)存操作不具有原子性,將可能會(huì)產(chǎn)生意想不到后果。請看下面示意圖:

http://wiki.jikexueyuan.com/project/java-memory-model/images/15.png" alt="" />

如上圖所示,假設(shè)處理器 A 寫一個(gè) long 型變量,同時(shí)處理器 B 要讀這個(gè) long 型變量。處理器 A 中64位的寫操作被拆分為兩個(gè)32位的寫操作,且這兩個(gè)32位的寫操作被分配到不同的寫事務(wù)中執(zhí)行。同時(shí)處理器B中64位的讀操作被分配到單個(gè)的讀事務(wù)中執(zhí)行。當(dāng)處理器 A 和 B 按上圖的時(shí)序來執(zhí)行時(shí),處理器B將看到僅僅被處理器 A “寫了一半“的無效值。

注意,在 JSR -133 之前的舊內(nèi)存模型中,一個(gè)64位 long/ double 型變量的讀/寫操作可以被拆分為兩個(gè)32位的讀/寫操作來執(zhí)行。從 JSR -133 內(nèi)存模型開始(即從 JDK5 開始),僅僅只允許把一個(gè)64位 long/ double 型變量的寫操作拆分為兩個(gè)32位的寫操作來執(zhí)行,任意的讀操作在 JSR -133 中都必須具有原子性(即任意讀操作必須要在單個(gè)讀事務(wù)中執(zhí)行)。

上一篇:基礎(chǔ)下一篇:總結(jié)