Java多執行緒(六) volatile

孤獨世界的深海流浪漢發表於2020-12-18

Java記憶體模型及volatile

問題:

  1. CPU 的 MESI 解決了什麼問題?它又帶來了什麼問題?
  2. 什麼是指令重排(重排序)?指令重排的作用是什麼?
  3. happens-before 是什麼?它解決了什麼問題?它有哪些規則?
  4. 什麼是 Java 記憶體模型,它解決了哪些問題?
  5. 什麼是記憶體屏障?它解決了什麼問題?
  6. volatile 如何防止指令重排?
  7. volatile 如何保持記憶體可見性? volatile 能保證原子性麼?
  8. 既然 CPU 有 MESI ,為什麼 Java記憶體模型還需要 volatile 關鍵字?
  9. volatile 主要的使用場景?
  10. volatile 和 synchronized 的區別?

1.CPU 的 MESI 解決了什麼問題?它又帶來了什麼問題?

在這裡插入圖片描述

  • CPU 處理速度遠大於主記憶體,為了解決速度差異,增加了多級別的快取(L1 L2 L3 快取)
  • MESI (CPU 快取一致性協議),保證了每個快取中使用的共享變數的副本是一致的。

1.1 CPU 快取一致性協議 MESI (Modified Exclusive Shared Or Invalid)

MESI 協議名字的由來是由其描述的四個 cache 狀態組成的。

狀態描述
M(Modified)當前 cache 的內容有效,資料已被修改而且與記憶體中的資料不一致,資料只在當前 cache 裡存在
E(Exclusive)當前 cache 的內容有效,資料與記憶體中的資料一致,資料只在當前 cache 裡存在
S(Shared)當前 cache 的內容有效,資料與記憶體中的資料一致,資料在多個 cache 裡存在
I(Invalid)快取行是無效的

1.2 MESI 帶來了什麼問題?

MESI 的設計簡單、直接,但是會導致效能下降

  • 將某個 Cache Line 標記為 Invalid 狀態,或者當某 Cache Line 當前狀態為 Invalid 時寫入資料。這兩個操作都是比較耗時的**,如果 CPU 在這兩個過程中一致等待的話,效能就會存在問題**。
  • CPU 通過 Store Buffer 和 Invalidate Queue 元件來降低這類操作的延時。

1.3 如何解決 MESI 帶來的效能問題?

在這裡插入圖片描述

  • CPU 通過 Store Buffer 和 Invalidate Queue 元件來降低這類操作的延時。

2.什麼是指令重排(重排序)?指令重排的作用是什麼?

自測:不影響方法執行結果,指令會重排,增強效能,減少記憶體使用。何時重排,指令間如果沒有關聯則可重排

指令重排:

  • 在執行程式時候為了提高效能,程式指令的執行順序有可能和程式碼的順序不一致,這個過程稱之為指令重排。
  • 指令重排作用: 充分利用多核 CPU,多級快取等進行適當的指令重排序,使得程式在保證業務執行的同時,最大限度提升效能。(多核CPU並行編譯)

例如 A1 為 i = 1 , A2 為 j = 2 , A3 為 k = i * j, A1與 A2無關係,A1、A2都與A3有依賴,則重排序後可能的情況有兩種; A1-> A2 -> A3 或者 A2->A1->A3。

重排序的分類:

  • 編譯器重排序。對於沒有依賴關係的語句,編譯器重新調整語句的執行順序。
  • CPU 指令重排序。 在指令級別,讓沒有依賴關係的多條指令並行執行。(利用多核CPU)
  • 記憶體系統的重排序。CPU 使用快取和讀/寫緩衝區,指令執行順序和寫入主記憶體順序不完全一致。
    在這裡插入圖片描述

3.happens-before 是什麼?它解決了什麼問題?它有哪些規則?

自測:先行發生原則,jvm預設保證的先行原則,不需要我們手動去做

happens - before:

  • 由於指令重排的特性,為了保證程式在多執行緒的條件下執行結果能夠和單一執行緒下一隻,引入了 happens-before 規則
  • happens-before 規則的主要目的是用來確保併發情況下資料的正確性
  • 指令重排用於提升效能,happens-before 用於保證資料的正確性

先行發生原則的八大規則:

  • 程式次序規則:一個執行緒內,按控制流順序。(一個執行緒內,寫在前面先行發生)
  • 管程鎖定規則:unlock操作先行發生於後面(時間上)對同一個鎖的lock操作
  • volatile變數規則:對一個volatile變數的寫操作先行發生於後面(時間上)對這個變數的讀操作
  • 執行緒啟動規則:Thread物件的start()方法先行發生於此執行緒的每一個動作
  • 執行緒終止規則:執行緒中的所有操作都先行發生於對此執行緒的終止檢測
  • 執行緒中斷規則:對執行緒interrupt()方法的呼叫先行發生於被中斷執行緒的程式碼檢測到中斷時間的發生,可以通過Thread.interrupted()方法檢測到是否有中斷髮生
  • 物件終結規則:建構函式執行結束先行於finalize()方法的開始
  • 傳遞性:A先行於B,B先行於C,則A先行於C

4.什麼是 Java 記憶體模型,它解決了哪些問題?

自測:主記憶體-工作記憶體-執行緒,資料之間的幾個動作 read load use assign store write

4.1 Java記憶體模型含義

Java記憶體模型,JMM 定義了執行緒和主記憶體之間的關係:執行緒之間的共享變數儲存在主記憶體中,每個執行緒都有一個私有的本地記憶體或者說工作記憶體,本地記憶體中儲存了該執行緒以讀/寫共享變數的副本

在硬體記憶體模型中,各種 CPU 架構的實現是不盡相同的, Java作為跨平臺的語言,為了遮蔽底層硬體差異,定義了 Java記憶體模型。 JMM 作用於 JVM 和底層硬體之前,遮蔽了下游不同硬體模型帶來的差異,為上游開發者提供了統一的使用介面。

JMM:
在這裡插入圖片描述

  • 1.所有的變數都儲存在主記憶體中
  • 2.每個執行緒都有自己獨立的工作記憶體,裡面儲存該執行緒使用到的變數的副本(副本為主記憶體中該變數的一份拷貝)
  • 3.執行緒對共享變數的所有操作都必須在自己的工作記憶體,不能直接相從相互記憶體中讀寫也不能在主記憶體中直接操作
  • 4.執行緒間變數值的傳遞需要通過主記憶體來完成

CPU架構與JMM記憶體模型對比理解

在這裡插入圖片描述

4.2 JMM解決了什麼問題?

JMM 解決了原子性、可見性、有序性這三個方面的問題。這三個特性也稱之為JMM的三大特性

4.3 JMM 產生的背景 —— 多核 CPU 工作的問題

  • 多核 CPU 運算期間,變數有多個副本,其他 CPU 無法感知變數的操作
  • 為了高效,進行指令重排,在多執行緒下將會產生無法預測的結果。

而JMM就解決了 原子性,可見性,有序性的三個問題:

  • 原子性:保證一系列操作要麼全部完成,要麼全部失敗。JMM 通過關鍵字 synchronized 實現原子性。 底層通過 monitorenter 和 monitorexit 命令實現。
  • 可見性:共享變數的修改對其他執行緒可見。JMM 通過 volatile 關鍵字保證對共享變數的修改,馬上重新整理到主記憶體,其他執行緒操作共享變數必須重新從主記憶體中獲取副本。 volatile 將 read_load_use 三個操作捆綁在一起,將 assign_store_write三個操作捆綁在一起。
  • 有序性:保證程式按照正確的順序執行。 實現精緻指令重排序可以通過 volatile 新增記憶體屏障、 happens-before 原則來實現。

5.什麼是記憶體屏障?它解決了什麼問題?

5.1 記憶體屏障(Memory Barrier)

  • 記憶體屏障是一組 CPU 指令,用於實現對記憶體操作的順序限制。
  • 大多數現代計算機為了提高效能而採取亂序執行,這使得記憶體屏障成為必須。

5.2 記憶體屏障作用及指令(可見性、有序性)

  • 記憶體屏障有兩個指令: Load Barrier(讀屏障),Store Barrier(寫屏障)

記憶體屏障的作用:

  • 阻止屏障兩側的指令重排序:(後面不往屏障前重排,前面不往屏障後重排)
  • 強制把寫緩衝區、快取記憶體中的髒資料等寫回主記憶體,讓快取中相應的資料失效。
  • 對於 Load Barrier 來說,在指令前插入 Load Barrier,強制重新從主記憶體載入資料,可以讓快取記憶體中的資料失效(不去讀快取中的資料,而是去主記憶體中重新載入)
  • 對於 Store Barrier 來說,在指令後插入 Store Barrier, 能讓寫入快取中的嘴型資料更新寫入主記憶體,讓其他執行緒可見。

四種記憶體屏障:

  • LoadLoad屏障:禁止讀和讀的重排序
  • StoreStore屏障:禁止寫和諧的重排序
  • LoadStore屏障:禁止讀和寫的重排序
  • StoreLoad屏障:禁止寫和讀的重排序

6.volatile 如何防止指令重排?

volatile 通過以下記憶體屏障策略,防止指令重排:

  • 在每個 volatile 寫操作之前插入 StoreStore屏障。保證 volatile 寫操作不會和之前的寫操作重排序。
  • 在每個 volatile 寫操作之後插入 StoreLoad 屏障。 保證 volatile 寫操作之後不會和之後的讀操作重排序。
  • 在每個 volatile 讀操作之後插入 LoadLoad 屏障、LoadStore屏障。保證 volatile 讀操作不會和之後的讀操作、寫操作重排序。

volatile重排序規則表:

在這裡插入圖片描述

  1. 當第二個操作是 volatile 寫,不管第一個操作是什麼,都不能重排序
  2. 當第一個操作是 volatile 讀,不管第二個操作是什麼,都不能重排序
  3. 當第一個操作是 volatile 寫,第二個操作是 volatile 讀,不能重排序

7.volatile 如何保持記憶體可見性? volatile 能保證原子性麼?

  • volatile 通過記憶體屏障,保證可見性。
  • volatile 不能保證原子性

8.既然 CPU 有 MESI ,為什麼 Java記憶體模型還需要 volatile 關鍵字?

MESI 只是保證多核 CPU 快取一致性的協議,它和 volatile 關鍵字定位不一樣。 volatile 是輕量級鎖,可以保證可見性和有序性。

9.volatile 主要的使用場景?

  • 狀態標誌: 實現 volatile 變數的規範使用僅僅是使用一個布林狀態標誌,用於只是發生了一個重要的一次性時間,例如完成初始化或請求停機。(例如我們不希望因為指令重排序導致 初始化完成的狀態標誌在真正初始化完成之前設為true,又例如 AQS 中的state 經常變的,也用 volatile來修飾)
  • 輕量級的讀-寫鎖:通過鎖來實習啊獨佔寫鎖,使用 volatile 實現共享的讀鎖(多個執行緒可以同時讀 value 值)
  • volatile bean 模式:中介軟體(框架)為易變資料的持有者提供了容器,但是放入這些容器中的物件必須是執行緒安全的。

10.volatile 和 synchronized 的區別?

  • volatile 是輕量級鎖,不會造成變成的阻塞;synchronized 是重量級鎖,可能會造成執行緒的阻塞。
  • volatile 保證變數的可見性,不保證原子性。而 synchronized 可以保證可見性和原子性。
  • volatile 僅能使用在變數級別; synchronized 可以使用在 變數,方法,程式碼塊和類。
  • volatile 變數修飾符如果使用恰當的話,它比 synchronized 的使用和執行成本更低,因為它不會引起執行緒上下文的切換和排程。

相關文章