何為內存模型(JMM)?


前言

任何一門語言都有其語言規范,從邏輯上我們可划分為語法規范和語義規范,語法規范則是描述了如何通過相關語法編寫可執行的程序,而語義規范則是指通過語法編寫的程序所構造出的具體含義。語言只要具備存儲(比如堆、棧),我們此時必須定義存儲行為規則,這種行為規則就是內存模型。Java初始版本內存模型允許行為安全泄漏,此外,它阻止了幾乎所有的單線程編譯器優化操作,因此,從Java 1.5開始,引入了新的內存模型來修復這些缺陷,接下來我們來詳細了解看看其內存模型到底是啥玩意,若有錯誤之處,還望批評指正。

內存模型(JMM)

我們知道大多數情況下編寫的程序按順序而執行,此時也是按照對應順序存儲在內存中,很顯然,讀取應遵循該順序進行的最新寫入,這是最原始的單核模型,隨着時代的進步、技術也才隨之發展,此時出現了多處理器體系結構,線程共享內存已凸顯出對於並發編程的優勢,但共享內存必然要使用同步機制使得內存中的數據一致,而同步機制卻對系統性能產生很大影響,為了避免這種情況,通過對應策略使得存儲數據一致性,當然這些策略是放寬的,如此將導致意外情況的出現,因為開發者很難推理執行程序最終結果,所以在多線程情況下我們尤其關心內存模型,但是問題隨之變得復雜了起來,內存模型定義了在實現該內存的共享內存體系結構上運行的多線程程序的所有可能結果,從本質上講,可認為它是對可能值的規范,它允許返回對內存的讀取訪問,從而指定平台的多線程語義。 Java內存模型(JMM)設計有兩個目標:應該允許盡可能多的編譯器優化、一般情況下開發者不必了解其所有復雜性可以更容易進行多線程編程,但是這項任務巨大,從某種意義上來講,這種模型增加了太多的不確定性,為了實現第二個目標,為了開發者能夠更好的可編程,於是JMM提供一個較弱的保證,稱之為無數據競爭保證(Data Race Free),簡稱為DRF,它保證:如果程序不包含數據競爭,則允許行為可以通過交錯語義來描述,換句話說,如果程序的所有順序一致的執行沒有數據爭用,則所有執行似乎都是順序一致的,所以我們可認為JMM是無數據爭用的內存模型,DRF通過順序一致性來保證。這里我們只是抽象概括了內存模型,接下來將通過大量的篇幅來進一步分析順序一致性以及通過順序一致性怎么就保證了內存模型。

順序一致性(Sequential Consistency簡稱SC)

我們知道在多線程情況下由於競爭條件的存在會發生數據競爭,那么如何解決數據競爭呢?這就涉及到數據競爭自由度(Data Race Freeness)的概念:程序結果計算的正確性依賴於運行時的相關時序或多線程交替時就會產生競爭條件從而發生數據競爭,那么反過來講,數據競爭自由度則是正確同步的程序具有順序一致性,那么到底何為順序一致性(sequential consistency)呢?執行結果都與所有處理器的操作相同按順序執行,每個操作處理器按程序指定的順序依次出現,單個內存引用(加載或存儲)完成的順序稱為執行順序,語句在原始代碼中的排序方式稱為程序順序(Program Order以下簡稱為PO),執行順序決定總的順序,執行順序與程序順序(每個線程中的指令順序所決定的順序)一致,並且每次讀取存儲位置時都會看到寫入的最后一個值,這意味着,執行順序好像具有按總順序(Total Order)執行的結果形態,但執行的實際順序不一定按總順序進行,因為編譯器的優化和指令的並行執行是允許的。講到這里,感覺很抽象,接下來我們通過簡單的例子來介紹執行順序、總順序、程序順序概念。

public class Main {
    public static void main(String[] args) {
        int x = 1;
        int y, z, m;
        if (x == 1) {
            y = 2;
        } else {
            z = 1;
        }
        z = y;
    }
}

上述我們初始化分配x==1,而y、z、m都等於0,接下來進入判斷語句,總順序則是由read(x):2、write(y,2)、read(y):2組成,程序順序就是總順序,而執行順序是實際在內存中的操作順序,由於緩存一致性協議的存在改善了系統處理性能,同時為了解決緩存副本需要使用同步機制使得緩存副本一致,但是同步機制大大降低了性能,於是通過重排序進一步改善性能,接踵而來的重排序能夠保持SC(順序一致性)嗎,我們看如下例子:

public class Main {
    public static void main(String[] args) {
        int a = 1;
        int b = 2;
        System.out.println(a);
        System.out.println(b);
    }
}

上述在多線程情況下進行重排序可能先打印出2,然后打印出1,重排序並未影響實際打印結果,重排序優化了性能,使得代碼運行的更快,但是要是如下例子呢?

class ReadWriteExample {
    int a, b = 0;

    void write() {
        b = 2;
        a = 1;
    }

    void read() {
        int r1 = a;
        int r2 = b;
        System.out.println(r1);
        System.out.println(r2);
    }
}

 如果按照SC定義在多線程情況下執行,要么a = 1最后執行,要么r2 = b最后執行,所以r1和r2可能的結果為(0,*)或者(*,2),但是若按照如下形式執行重排序,此時r1和r2的結果為(1,0),所以我們可得出結論:重排序並不能保持SC,可能會打破SC

因為指令重排序的可能性,所以順序一致性並不意味着操作按照特定的總順序執行,盡管順序一致性具有非常清晰而明確的語義,但編譯器很難靜態地確定它是否會進行指令重排序或允許並行執行操作以保留安全性,這種語義阻止了許多(但不是全部)針對順序代碼的常見編譯器優化,因此,對於未正確同步(即包含數據競爭)的程序,對順序一致性采取了比較弱的含義。那么問題來了,Java中的內存模型究竟強內存模型還是弱內存模型呢?操作系統的內存模型是強內存模型,因為它對正常內存操作和同步操作做出明確的區分,而Java的內存模型是弱內存模型,它對正常內存操作和同步操作沒有做出具體的區分,尤其是針對同步操作,它僅僅只提供了指導方向或基本思想即:同步操作在執行過程中要引起其他操作的可見性和順序一致性限制。在弱內存模型中,並不對所有動作進行排序,僅對一些有限的原語施加硬性排序,在JMM中,這些原語包裝在它們各自的同步動作中。接下來我們進入到進入到利用同步操作構建弱模型的排序情況。在《Java並發編程實戰》一書中對內存模型的定義為:通過動作的形式進行描述的、所謂動作,包括變量的讀寫、監視器的加鎖和釋放鎖、線程的啟動和拼接,這里指代的動作即為同步動作(Synchronization Action),通過volatile進行讀寫、通過lock進行讀鎖和釋放鎖、線程的啟動(thread.start)、線程的終止(thread.join)。既然講到同步動作對內存模型的定義,那么逃脫不了對同步順序(Synchronization Order以下簡稱為SO)的詳細了解,因為同步操作來源於同步順序,同步順序(SO)是涵蓋所有同步操作的總順序,JMM提供了兩個附加約束:SO-PO一致性和SO一致性。 接下來我們通過簡單的例子來解開這些約束。

class ReadWriteExample {
    volatile int x, y = 0;

    void m1() {
        x = 1;
        int r1 = y;
    }

    void m2() {
        y = 1;
        int r2 = x;
    }
}

上述通過volatile關鍵字修飾變量x和y,因此滿足SO,正常PO是write(x,1)、read(y,?)、write(y,1)、read(x,?),但是SO則可能是:【write(x,1)、read(y,?)、read(x,?)、write(y,1),SO與PO執行不一致】和【write(x,1)、read(y,?)、write(y,1)、read(x,?),SO與PO執行一致】和【write(x,1)、write(y,1)、read(x,?)、read(y,?),SO與PO執行一致】。通過分析我們知道SO-PO一致性就是和正常執行程序操作一樣,而SO一致性告訴我們要知道SO所有在此之前的動作,尤其是在不同線程中。所以我們可推出:同步操作(SA)是順序一致性(SC)的,在聲明為volatile的變量程序中,我們可以對結果進行推理,由於SA是SC,通過SO就足以推理結果,即使是所有動作進行了交替執行。然而SO並不能構建實用的弱內存模型,只能說SO構建了弱內存模型的基本骨架,其原因是:要么將所有操作轉換為SA,要么讓非SA操作不受限制的進行排序,很顯然這樣還是會破壞程序實際結果,若為了達到SC,需要將整個程序進行鎖定,但是這又以犧牲性能為代價。接下來我們繼續看看如下例子:

class ReadWriteExample {
    int x;
    volatile int y;

    void m1() {
        x = 1;
        y = 1;
    }

    void m2() {
        int r1 = y;
        int r2 = x;
    }
}

由於我們通過volatile修改了變量y,所以會對y進行SO,我們可以正確讀取到y = 1,但是對於非SA操作即x變量的值讀取,到底是0還是1呢?不得而知。SO即使以犧牲性能為代價保證了順序一致性,但對非SA操作還需要一個非常弱的語義保證,那就是事先發生(happens-before)。那么什么是事先發生呢?為了捕獲有關內存操作的基本順序和可見性要求,JMM基於事先發生規則(happens-before)【深入理解Java虛擬機將其翻譯為先行發生】, 此規則確定了在任何其他動作之前必須發生的動作,換句話說,此順序指定任何讀取必須看到的內存更新,僅允許執行不違反此順序的命令。由於此模型提供的保證非常弱,因此可允許單線程編譯器優化,但是,發生在模型允許通過執行動作的循環證明而憑空產生值的執行之前發生,為避免此類循環證明並保證DRF,目前的JMM比模型初始版本內存模型處理起來要復雜得多。那么事先發生規則是如何解決非SA操作的問題呢?通過引入SO的子順序來描述數據的流轉或者說以此來連接線程之間的狀態,我們稱之為Synchronization-with Order簡稱為SW,構造SW相當容易,SW並不是完整的順序,不能覆蓋所有同步操作對。我們繼續來看上述代碼,SW僅對看到的彼此進行操作配對,例如如上通過volatile修飾的變量y,對變量y的寫入與隨后所有y的讀取都將同步,所以SW是根據SO來定義的,由於SO的一致性,對於變量y寫入1僅與讀取1同步,在此示例中,我們看到了讀取和寫入兩個操作之間的SW,該子順序為我們提供了線程之間的橋梁,但適用於同步操作,同樣也可以擴展到非SA操作,上述程序在多線程情況下,將通過PO和SW的並集而得到HB(happens-before),從某種意義上講,HB同時獲得線程間和線程內的語義,PO將與每個線程內的順序操作有關的信息傳輸到HB中,而在狀態同步時通過SW傳輸,HB是部分排序,並允許使用指令重排序的動作構造等效執行,所以上述可能的執行順序是(write(x,1)、write(y,1)、read(y,1)、read(x,1)),再通俗一點講則是,當執行寫入y時,由於SW(基於SO)的存在立馬對y的讀取完全可見,所以整個HB順序由寫入x到寫入y,很自然的過度到讀取y和讀取x,這是可能性情況之一,如果在執行HB一致性之前就進行了讀取,那么x和y值可能就是0和0,這屬於HB一致性的邊界情況分析,當然,有的時候結果也會出乎意料,比如在進行x的讀取和寫入時沒有做到HB,可能結果為0和1,那說明存在競爭,換句話說沒有遵守HB一致性,因此不能用來推斷結果。謹記不要將SO一致性和HB一致性概念混淆:SO一致性規則指出同步操作應查看SO中最新的相關內容,而HB一致性規則指出它指示特定讀取可以觀察到哪些寫入。

 

Java內存模型定義了8種操作(lock、unlock、read、load、use、assign、store、write)來進行主內存和工作內存的交互細節且為原子性,同時針對8種操作之間的規則限定實踐起來非常繁瑣,所以最終通過引入事先發生(happens-before)規則來保證在並發下的線程安全,關於以上8種操作的細節請參看《深入理解Java虛擬機》一書。本文詳細介紹了內存模型就是一種規范,提供了可能允許的結果,它的本質是提供了一個弱的DRF保證即順序一致性,而順序一致性則是通過事先發生(happens-before)來保證,而事先發生(happens-before)則是8大規則:程序次序規則、監視器鎖定規則、volatile變量規則、線程啟動規則、線程終止規則、線程中斷規則、對象終結規則、傳遞性。那么內存模型的具體含義是什么呢?JMM正式定義的基本組成部分是:動作,執行和驗證動作的提交過程,而提交過程又會驗證完整的執行。而動作則是(例如對變量的賦值),而動作匯總於執行,驗證執行通過(po、so、sw、hb)有效執行產生所需的結果,最終提交允許該結果。

 

總結

本文我們步步分析而引入JMM的概念及其本質,最重要的是我們需要明確知道幾個概念:PO、SO、SW、HB,這幾個概念就是對DRF的保證, 程序順序(PO)是對線程內的語義描述,同步順序(SO)是同步操作的總順序,同步子順序(SW)是基於SO連接線程狀態的橋梁,事先發生(HB)是操作有序性的保障。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM