天天看點

Java面試-記憶體模型之順序一緻性

訂閱專欄

簡介:

順序一緻性記憶體模型是一個理論參考模型,處理器的記憶體模型和程式設計語言的記憶體模型都會以順序一緻性記憶體模型作為參照。

1、資料競争和順序一緻性

當程式未正确同步時,就可能存在資料競争。

1.1 Java記憶體模型規範對資料競争的定義如下

在一個線程中寫一個變量

在另一個線程中讀同一個變量

寫和讀沒有通過同步來排序

如果一個多線程程式能夠正确同步,這個程式将是一個沒有資料競争的程式,往往存在資料競争的程式,運作結果與我們的預期結果都會存在偏差。

1.2 JMM對多線程程式的記憶體一緻性做的保證

如果程式正确同步(正确使用synchronized、volatile和final),程式的執行将具有順序一緻性(Sequentially Consistent)——即程式的執行結果與該程式在順序一緻性記憶體模型中的執行結果相同。

2、順序一緻性記憶體模型

2.1 特性

一個線程中的所有操作必須按照程式的執行順序來執行

(不管是否正确同步)所有的線程都隻能看到一個單一的操作執行順序,每個操作都必須原子執行且立刻對所有線程可見。

圖示:

Java面試-記憶體模型之順序一緻性

順序一緻性記憶體模型視圖

在概念上,順序一緻性模型有一個單一的全局記憶體,這個記憶體通過一個左右擺動的開關可以連接配接到任意一個線程,同時每一個線程必須按照程式的順序來執行記憶體的讀/寫操作。上圖中可以看出, 在任意時刻最多隻有一個線程可以連接配接到記憶體。是以,在多線程并發執行時,圖中的開關裝置能把所有的記憶體讀/寫操作串行化(即在順序一緻性模型中所有操作之間具有全序關系)。

2.2 舉例說明順序一緻性模型

假設兩個線程A和B并發執行。其中

A線程的操作在程式中的順序為:A1 - A2 - A3

B線程的操作在程式中的順序為:B1 - B2 - B3。

假設線程A和線程B使用螢幕鎖來正确同步,A線程的3個操作執行後釋放螢幕鎖,随後B線程擷取同一個螢幕鎖。那麼程式在順序一緻性模型中的執行效果如下所示:

Java面試-記憶體模型之順序一緻性

順序一緻性模型的一種執行效果

假設線程A和線程B沒有做同步,那麼這個未同步的程式在順序一緻性模型中的另一種可能的效果如下所示:

Java面試-記憶體模型之順序一緻性

順序一緻性模型的另一種執行效果

未同步程式在順序一緻性模型中雖然整體執行順序是無序的,但是所有線程都隻能看到一個一直的整體執行順序。以上圖為例,線程A和B看到的執行順序都是:A1 - B1 - A2 - B2 - A3 - B3。之是以能得到這個保證是因為順序一緻性記憶體模型中的每個操作必須立即對任意線程可見。

但是,在JMM中就沒有這個保證。**未同步程式在JMM中不但整體的執行順序是無序的,而且所有線程看到的操作執行順序也可能不一緻。**比如,在目前線程把寫過的資料緩存在本地記憶體中,在沒有重新整理到主記憶體之前,這個寫操作僅對目前線程可見;從其他線程的角度來觀察,會認為這個寫操作根本被目前線程執行。隻有目前線程把本地記憶體中寫過的資料重新整理到主記憶體之後,這個寫操作才能對其他線程可見。這種情況就會出現多種運作結果。

2.3 同步程式的順序一緻性效果

對上一章的ReorderExample程式用鎖來同步

package com.lizba.p1;

/**
 * <p>
 *      同步示例
 * </p>
 *
 * @Author: Liziba
 * @Date: 2021/6/8 21:44
 */
public class SynReorderExample {

    // 定義變量a
    int a = 0;
    // flag變量是個标記,用來标志變量a是否被寫入
    boolean flag = false;

    public synchronized void writer() {     // 擷取鎖
        a = 1;
        flag = true;
    }                                       // 釋放鎖

    public synchronized void reader() {     // 擷取鎖
        if (flag) {
            int i = a * a;
            System.out.println("i:" + i);
        }
    }                                       // 釋放鎖

}
      

測試代碼

/**
  * 測試
  *
  * @param args
  */
public static void main(String[] args) {

    final SynReorderExample re = new SynReorderExample();

    new Thread() {
        public void run() {
            re.writer();
        }
    }.start();

    new Thread() {
        public void run() {
            re.reader();
        }
    }.start();
}
      

執行多次結果結果都為1

Java面試-記憶體模型之順序一緻性

總結

在上面的示例代碼中,假設A線程執行writer()方法後,B線程執行reader()方法。這是一個正确同步的多線程程式。根據JMM規範,該程式的執行結果将與該程式在順序一緻性記憶體模型中的執行結果相同。

Java面試-記憶體模型之順序一緻性

順序一緻性模型中和JMM記憶體模型中的執行時序圖

在順序一緻性模型中,所有操作完全按程式的順序串行執行。而在JMM中,臨界區内的代碼可以重排序(但JMM不允許臨界區的代碼“逸出”到臨界區之外,那樣會破壞螢幕鎖的語義)。JMM會在進入臨界區和退出臨界區的關鍵時間點做一些特殊處理,使得線程在這兩個時間點具有順序一緻性模型中相同的記憶體視圖。雖然線程A在臨界區内做了重排序,但由于監視鎖互斥執行的特性,這裡線程B無法“觀察”到線程A在臨界區内的重排序。JMM在具體實作上的基本方針為:在不改變(正确同步)程式執行結果的前提下,盡可能為編譯器和處理器的優化打開友善大門。

2.4 未同步程式的執行特性

對于未同步或者未正确同步(代碼寫錯了的兄弟們),JMM隻提供最小的安全性:

線程執行時讀取到的值不會無中生有(Out Of Thin Air)

之前某個線程寫入的值

預設值(0、Null、False)-- JVM會在已經清零了記憶體空間(Pre-zeroed Memory)配置設定對象。

未同步程式在兩個模型中的執行特性對比

Java面試-記憶體模型之順序一緻性

第三個差異和總線的機制有關。在一些32位處理器上,處理64位的資料寫操作,需要将一個寫操作拆分為兩個32位的寫操作。

3、 64位long型和double型變量寫原子性

3.1 CPU、記憶體和總線簡述

在計算機中,資料通過總線在處理器和記憶體之間傳遞,每次處理器和記憶體之間的資料傳遞都是通過一系列的步驟來完成的,這一系列的步驟稱之為總線事務(Bus Transaction)。總線事務包括讀事務(Read Transaction)和寫事務(WriteTransaction),事務會讀\寫記憶體中一個或多個實體上連續的字。

讀事務 → 記憶體到處理器

寫事務 → 處理器到記憶體

重點是,總線會同步試圖并發使用總線的事務。在一個處理器執行總線事務期間,總線會禁止其他處理器和I\O裝置執行記憶體的讀\寫。

Java面試-記憶體模型之順序一緻性

總線工作機制

由上圖所示,假設處理器A、B、C、D同時向總線發起總線事務,這時總線總裁(Bus Arbitration)會對競争作出裁決,這裡假設處理器A在競争中獲勝(總線仲裁會確定所有處理器能公平通路記憶體)。此時處理器A繼續它的總線事務,而其他所有的總線事務必須要等待A的事務完成才能再次執行記憶體的讀\寫操作。總線事務工作機制確定處理器對記憶體的通路以串行的方式執行。在任意時間點都隻有一個處理器可以通路記憶體,這個特性能確定總線事務之間的記憶體讀\寫操作具有原子性。

3.2 long和double類型的操作

在一些32位的處理器上,如果要求對64位資料的寫操作具有原子性,那麼會有非常大的同步開銷。Java語言規範中鼓勵但不強求JVM對64位long型和double類型的變量寫操作具有原子性。當JVM在這種處理器上運作時,會把一個64位的變量寫操作拆成兩個32位寫操作來執行,此時寫不具備原子性。

Java面試-記憶體模型之順序一緻性

總線事務執行的時序圖

存在問題:

假設處理器A寫一個long類型的變量,同時處理器B要讀這個long類型的變量。處理器A中64位的寫操作被拆分成兩個32位的寫操作,且這兩個32位的寫操作被配置設定到不同的事務中執行。此時,處理器B中64位的讀操作被配置設定到單個讀事務中執行。如果按照上面的執行順序,那麼處理器B讀取的将會是一個不完整的無效值。

處理方式:

JSR-133記憶體模型開始(JDK1.5),寫操作能拆分成兩個32位寫事務執行,讀操作必須在單個事務中執行。