天天看點

并發程式設計-緩存一緻性協定和Java記憶體模型

并發程式設計

為什麼要使用并發程式設計?

  1. 充分利用CPU的計算能力。
  2. 友善進行業務拆分,提升應用性能。

并發程式設計特性:

  • 原子性:是一個操作是不可中斷的,即使是在多線程環境下,一個操作一旦開始就不會被其他線程影響。

    對于32位系統的來說,long和double資料類型的讀寫是非原子性的

    。可以通過

    synchronized

    Lock

    實作原子性。
  • 可見性:是當一個線程修改了某個共享變量的值,其它線程能夠馬上得知這個修改的值。

    volatile

    關鍵字可以保證可見性。

    synchronized

    Lock

    也可以保證可見性,因為它們可以保證任一時刻隻有一個線程能通路共享資源,并在其釋放鎖之前将修改的變量重新整理到記憶體中。
  • 有序性:指對于單線程來說,代碼的執行是按順序依次執行。對于多線程環境,則可能出現亂序現象,因為程式編譯成機器碼指令後可能會出現指令重排現象,重排後的指令與原指令的順序未必一緻。

    volatile

    關鍵字可以保證一定的有序性,

    synchronized

    (synchronized代碼塊内部的有序性無法保證)和

    Lock

    也可以保證有序性,synchronized和Lock保證每個時刻隻有一個線程執行同步代碼。

并發場景下存在哪些問題?

  1. 上下文頻繁切換。
  2. 臨界區線程安全問題,容易出現死鎖,使用

    jstack

    可以排查死鎖。
/**
 * @author alex
 * @description : 并發死鎖示例
 * @date 2021年04月26日17:53
 */
public class DeadLockTest {
    public static final String A = "a";
    public static final String B = "b";

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            synchronized (A) {
                System.out.println("get A from t1");
                try {
                    Thread.sleep(2000);
                    synchronized (B) {
                        System.out.println("get B from t1");
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        Thread t2 = new Thread(() -> {
            synchronized (B) {
                System.out.println("get B from t2");
                synchronized (A) {
                    System.out.println("get A from t2");
                }
            }
        });

        t1.start();
        t2.start();
    }
}
           

CPU多核緩存架構

并發程式設計-緩存一緻性協定和Java記憶體模型
電腦緩存是當cpu在讀取資料的時候,先是從緩存檔案中查找,然後找到之後會自動讀取,再輸入到cpu進行處理,當然如果沒有在緩存中找到對應的緩存檔案的話,那麼就會從記憶體中讀取并且傳輸給cpu來處理。當然這樣的話需要一定的時間是以會很慢。等cpu處理之後,就很快把這個資料所在的資料塊儲存在緩存檔案中,這樣的話在以後讀取這項資料的時候就直接在緩存中進行,不要重複在記憶體中調用并讀取資料了。
  • 一級緩存都内置在CPU内部并與CPU同速運作,可以有效的提高CPU的運作效率。一級緩存越大,CPU的運作效率越高,但受到CPU内部結構的限制,一級緩存的容量都很小。
  • 二級緩存,它是為了協調一級緩存和記憶體之間的速度。cpu調用緩存首先是一級緩存,當處理器的速度逐漸提升,會導緻一級緩存就供不應求,這樣就得提升到二級緩存了。二級緩存它比一級緩存的速度相對來說會慢,但是它比一級緩存的空間容量要大。主要就是做一級緩存和記憶體之間資料臨時交換的地方用。
  • 三級緩存是為讀取二級緩存後未命中的資料設計的—種緩存,在擁有三級緩存的CPU中,隻有約5%的資料需要從記憶體中調用,這進一步提高了CPU的效率。其運作原理在于使用較快速的儲存裝置保留一份從慢速儲存裝置中所讀取資料并進行拷貝,當有需要再從較慢的儲存體中讀寫資料時,緩存(cache)能夠使得讀寫的動作先在快速的裝置上完成,如此會使系統的響應較為快速。

當多個CPU對同一個變量進行操作時,會出現資料不一緻的情況,此時該如何處理?

  1. 總線加鎖

    阻塞其他CPU,使該處理器可以獨享此共享記憶體。
  2. 緩存一緻性協定

緩存一緻性協定

這類協定有MSI、MESI(IllinoisProtocol)、MOSI、Synapse、Firefly及DragonProtocol等,其中

MESI

是緩存一緻性協定最為流行的一種實作方式。
并發程式設計-緩存一緻性協定和Java記憶體模型

MESI工作原理:

  • M: Modified 修改
  • E: Exclusive 獨享、互斥
  • S: Share 共享
  • I: Invalid 無效
  1. 當一塊記憶體被某一個CPU(假設為CPU1)讀取到緩存中時,會将此記憶體标記為

    E

    ,表示記憶體被獨享,此時該CPU也會同時監聽其它CPU對這個記憶體的操作。
  2. 當其它CPU(假設為CPU2)讀取同一塊記憶體到緩存中時,CPU1會監聽到這個動作,此時CPU1和CPU2都會将這塊記憶體的狀态标記為

    S

  3. 當CPU1對這塊記憶體進行了修改,并且需要将修改結果寫入主記憶體中時,此時CPU1将這塊資料标記為

    M

    ,并且對緩存行進行加鎖(彙編指令#Lock),告知緩存一緻性協定需要将結果寫入主記憶體中,CPU2監聽到這一操作時會将CPU2緩存中對這塊資料的标記從

    S

    改為

    I

  4. 當CPU1寫入完成時,CPU1緩存中對這塊資料的标記從

    M

    改為

    E

    ,若CPU2還需要讀取這塊記憶體資料,則需要重新從記憶體中加載,CPU2加載到緩存時,CPU1和CPU2會将這塊記憶體的狀态标記為

    S

  5. 若CPU1和CPU2需要同時更改,那麼在同一個指令周期内會進行裁決,決定由誰進行修改,另一個CPU修改變成無效。

什麼情況下緩存一緻性協定會失效?

  • 如果讀取的記憶體存儲長度大于一個緩存行時,可以使用總線加鎖的方式
  • CPU不支援緩存一緻性協定時,如奔騰處理器。

線程

程序

是系統配置設定資源的基本機關。

線程

是排程CPU的基本機關。一個程序中至少包含一個執行線程。每個線程都有一個程式計數器(記錄要執行的下一條指令),一組寄存器(儲存目前線程的工作變量),堆棧(記錄執行記錄,其中每一幀儲存了一個已經調用但未傳回的過程,棧幀的數量與方法的調用次數一緻)

線程依賴于作業系統排程CPU。

線程分為兩類:

  1. 使用者級線程(User-Level Thread)
  2. 核心線線程(Kernel-Level Thread)

使用者線程:指不需要核心支援而在使用者程式中實作的線程,其不依賴于作業系統核心,應

用程序利用線程庫提供建立、同步、排程和管理線程的函數來控制使用者線程。另外,使用者線程是由應用程序利用線程庫建立和管理,不依賴于作業系統核心。不需要使用者态/核心态切換,速度快。作業系統核心不知道多線程的存在,是以一個線程阻塞将使得整個程序(包括它的所有線程)阻塞。由于這裡的處理器時間片配置設定是以程序為基本機關,是以每個線程執行的時間相對減少。

核心線程 :線程的所有管理操作都是由作業系統核心完成的。核心儲存線程的狀态和上下

文資訊,當一個線程執行了引起阻塞的系統調用時,核心可以排程該程序的其他線程執行。在多處理器系統上,核心可以分派屬于同一程序的多個線程在多個處理器上運作,提高程序執行的并行度。由于需要核心完成線程的建立、排程和管理,是以和使用者級線程相比這些操作要慢得多,但是仍然比程序的建立和管理操作要快。大多數市場上的作業系統,如Windows,Linux等都支援核心級線程。

Java線程的生命周期

并發程式設計-緩存一緻性協定和Java記憶體模型

Java記憶體模型-JMM

Java記憶體模型(Java Memory Model簡稱JMM)是一種抽象的概念,并不真實存在,它描述的是一組規則或規範,通過這組規範定義了

程式中各個變量(包括執行個體字段,靜态字段和構成數組對象的元素)的通路方式

。JVM運作程式的實體是線程,而每個線程建立時JVM都會為其建立一個工作記憶體(有些地方稱為棧空間),用于存儲線程私有的資料,而Java記憶體模型中規定所有變量都存儲在主記憶體,主記憶體是共享記憶體區域,所有線程都可以通路,但線程對變量的操作(讀取指派等)必須在工作記憶體中進行,首先要将變量從主記憶體拷貝的自己的工作記憶體空間,然後對變量進行操作,操作完成後再将變量寫回主記憶體,不能直接操作主記憶體中的變量,工作記憶體中存儲着主記憶體中的變量副本拷貝,前面說過,工作記憶體是每個線程的私有資料區域,是以不同的線程間無法通路對方的工作記憶體,線程間的通信(傳值)必須通過主記憶體來完成。

Java記憶體模型互動操作:

  • lock(鎖定):作用于主記憶體,将一個變量辨別為線程獨占。
  • unlock(解鎖):主記憶體,釋放一個處于鎖定狀态的變量。
  • read(讀取):主記憶體,将變量從主記憶體傳輸到工作記憶體。
  • load(載入):工作記憶體,将read讀到的值在工作記憶體中生成一個副本。
  • use(使用): 工作記憶體,将工作記憶體的變量傳遞給執行引擎。
  • assign(指派):工作記憶體,将執行引擎傳回的值指派給工作記憶體的變量。
  • store(存儲):工作記憶體,将工作内中存變量的值傳輸給主記憶體。
  • write(寫入):主記憶體,将store操作從工作記憶體中傳輸的值寫入主記憶體的變量中。