1. 程式人生 > 程式設計 >Java記憶體模型(一) - 一個碼農的期望

Java記憶體模型(一) - 一個碼農的期望

最近發現自己在產品思維上有所欠缺,並且剛剛讀完《java併發程式設計藝術這本書》,於是決定從程式設計師的需求角度大概寫寫自己對JMM的理解,希望對大家有所幫助。

背景知識

瞭解一個解決方案首先得明白它處理的問題,通過背景知識,我們可以更好的理解它產生的原因,以及其中所包含的思想

在單個處理器的處理速度的提升已經不在明顯的時候,人們便嘗試使用多種方法來提高計算機的運算效率,其中包括

  • 引入多級快取機制,同過減少了讀取運算資料、儲存運算結果等I/O操作的次數,讓計算機運算快速執行,減少了處理器等待時間
  • 使用多核處理器,三個臭皮匠頂過頂過一個諸葛亮
  • 程式碼亂序優化,重新調整順序的程式碼可以使用硬體底層的批處理等技術,加快運算效率

但同時一個全新概念的引入,往往會伴隨著諸多問題等待著這些偉大的先驅者去解決。

快取一致性問題

Java記憶體模型
10/13/16dc57096e9b6482?w=1003&h=716&f=png&s=54796)

當多個處理器要使用和修改主記憶體中同一塊區域時,可能會導致不同處理器中的資料不一樣,如何將不同CPU暫存器、快取記憶體中的資料進行同步,保證資料的一致性,運算的正確性的同時保證處理器的效率是設計著需要考慮的問題。在硬體上,各個處理器都遵循了各自的一致性協議,來解決這些問題

指令重排序問題

# CPU1            CPU1
a = 2             b = 100
b = 100
a = 2 print(a + b) print(a + b) 複製程式碼

重排序能夠有效的提高處理器的運算效率,但是重排序有時候會讓結果發生錯誤,尤其是在多核環境下,會發生很多奇妙的問題,猜猜下面兩個CPU輸出的結果

# CPU1                      CPU2
a = 10                      b = 20
b = a + 100                 a = b + 100
print(f"b = {b}")           print(f"a = {a}")
複製程式碼

如果已多次執行以上程式會發現,在終端中竟然出現了 a = 100,b = 100的結果,原因是底層處理器”自以為是“的重排序使得我們執行的程式變成如下

# CPU1                      CPU2
a = 10                      b = 20
print(f"b = {b}")           print(f"a = {a}")
b = a + 100                 a = b + 100
複製程式碼

各種硬體的區別

為解決多核處理器所帶來的問題,不同的設計著提供了不同的方案,但對於程式設計師,瞭解這些策略並編寫正確的程式具有很大的難度

程式設計師對JMM的期望

在生活中我們常常做不了甲方,但對於JAVA的設計者們來說,我們就是甲方,而且我們的要求不多,也並不過分

順序一致性

當我們程式設計師寫出了正確的程式碼的前提下,希望程式按照我們所設想的程式碼順序執行,並輸出正確的結果

所以在設計的時候,處理器的記憶體模型和程式語言的記憶體模型都會以順序一致性作為參考,接下我們具體的談談順序一致性的具體要求

資料讀寫與順序一致性

當我們處理多執行緒問題的時候,常常會遇到如下資料競爭的問題:

  • 執行緒1: 讀取記憶體區域的x變數
  • 執行緒2: 更新記憶體預取的x變數
  • 以上兩個操作如何同步?

作為程式設計師我們期望,當我們約定了執行緒1的操作先於執行緒2執行(反之亦然),此時這個程式便成為了一個沒有資料競爭的程式,同時我們也可以稱之為正確同步的程式,該程式的執行將具有順序一致性

執行緒與順序一致性

當我們運行了多個執行緒的時候,順序一致性向我們保證了:

  • 一個執行緒的所有操作必須按照程式的順序來執行(無指令重排序),所有操作只能看到一個單一的操作執行順序(將並行轉化為序列)
  • 每個操作必須是原子執行且立刻對所有執行緒可見。(注:原子執行是指這種操作一旦開始,就一直執行到結束,中間不會有任何執行緒切換)

![原子執行](user-gold-cdn.xitu.io/2019/

同步塊和順序一致性

class SynchronizedExample {
    int a = 0;
    boolean flag = false;

    public synchronized void writer() {
        a = 1;
        flag = true;
    }

    public synchronized void reader() {
        if (floag) {
            int i = a;
            ...
        }
    }
}
複製程式碼

在上面例項程式碼中,writer()和reader()方法,分別線上程1和執行緒二中執行,我們也期望同步塊中的執行也滿足執行緒的順序一致性

程式碼的執行效率

在滿足順序一致性的條件下,我們希望程式碼的執行效率越快越好

程式碼的可讀性和易實現

我們希望JAVA的設計者們能夠提供安全且易實現的機制,滿足順序一致性以及執行效率,並且讓我們的寫出來的程式碼具有良好的可讀性。

JMM的誕生

效率與一致性的博弈

從上文我們可以看到,順序一致性與執行效率是相互矛盾的,所以JAVA的設計者們做了如下決定

在滿足執行結果與程式設計師期望結果的前提下,儘可能的減少順序一直性對記憶體模型的束縛

為此,提出了happens-before原則來描述這種設計理念,本文不會針對這個概念做深入的解釋,但我們可以這麼理解,設計者們為程式設計師創造了一個程式是按照順序一致性執行的桃花源,它不會違背程式設計師所期望的結果,但實際過程卻有所不同。

語言設計

java設計者們為達到可讀性和易實現性,提供了一下關鍵字和方法

  • synchronized 關鍵字
  • volatile 關鍵字
  • final 關鍵字
    • concurrent包

JMM就是由這些小部件組成

總結

本文主要介紹了JMM誕生的背景知識,主要介紹了硬體方面的內容,並從程式設計師的角度分析,JMM應該滿足哪些條件,以及它的設計的總體思想

參考