😀 Java並發 – (並發基礎)

Java並發 – (並發基礎)

1、什麼是共享資源

  • 堆是被所有執行緒共享的一塊記憶體區域。在虛擬機啟動時創建。此記憶體區域的唯一目的就是存放對象實例

  • Java中幾乎所有的對象實例都在這裡分配記憶體。方法區與堆一樣,也是各個執行緒共享的一塊記憶體區域,它用於存儲已被虛擬機載入的類型資訊、常量、靜態變數、即時編譯器編譯後的程式碼快取等數據。

  • 光看文字,會讓我們覺得很抽象。如下圖:

    image-20221116191523993

2、並發編程的難點

🤔 原子性問題

  • 作業系統做任務切換(CPU切換),可以發生在任何一條CPU指令執行完成後;
  • CPU能保證的原子操作是指令級別的,而不是高級語言的操作符(例如:n++)。
  • 如下圖,是n++編譯後,被編譯CPU執行的指令。

image-20221116194015588

🧐 可見性問題

  • 可見性是指一個執行緒對共享變數的修改,另外一個執行緒能夠立刻看到。
  • 可見性問題是由CPU的快取導致的,多核CPU均有各自的快取,這些快取均要與記憶體進行同步。

image-20221116202546557

🧡 有序性問題

  • 在執行程式時。為了提高性能,編譯器和處理器常常會對指令做重排序;
  • 重排序不會影響單執行緒的執行結果,但是在並發情況下,可能會出現詭異的BUG。
  • 參考地址://zhuanlan.zhihu.com/p/298448987

3、JMM

💛 並發編程的關鍵目標

並發編程需要處理兩個關鍵問題,即執行緒之間如何通訊和同步。

  • 通訊:指執行緒之間以何種機制來交換資訊;
  • 同步:指程式中用於控制不同執行緒之間的操作發生的相對順序的機制。

💜 並發編程的記憶體模型

共有兩種並發編程模型:共享記憶體模型、消息傳遞模型,Java採用的是前者。

  • 在共享記憶體模型下,執行緒之間共享程式的公共狀態,通過寫-讀記憶體中的公共狀態進行隱式通訊;
  • 在共享記憶體模型下,同步是顯示進行的,程式設計師必須顯示指定某段程式碼需要在執行緒之間互斥執行。

❣️ 這個記憶體模型:JMM

JMM 是Java Memory Model的縮寫,Java執行緒之間的通訊由 JMM 控制,即 JMM決定一個執行緒對共享變數的寫入何時對另一個執行緒可見。JMM定義了執行緒和主記憶體之間的抽象關係,通過控制主記憶體與每個本地記憶體(抽象概念)之間的交互,JMM為Java程式設計師提供了記憶體可見性的保證。

image-20221116213852346

🔵 源程式碼與指令間的重排序

為了提高性能,編譯器和處理器常常會對指令做重排序。重排序有3種類型,其中後2種都是處理器重排序。這些重排序可能會導致多執行緒程式出現記憶體可見性問題。

  1. 編譯器優化重排序:編譯器在不改變單執行緒程式語義的前提下可以重新安排語句的執行順序。
  2. 指令級並行重排序︰現代處理器採用了指令級並行技術來將多條指令重疊執行,如果不存在數據依賴性,處理器可以改變語句對應機器指令的執行順序。
  3. 記憶體系統的重排序:由於處理器使用快取和讀/寫緩衝區,這使得載入和存儲操作看上去可能是在亂序執行。

image-20221116214553907

🍉 重排序對可見性的影響

參考下圖,雖然處理器執行的順序是A1->A2,但是從記憶體角度來看,實際發生的順序是A2->A1。這裡的關鍵是,由於寫緩衝區僅對自己的處理器可見,它會導致處理器執行記憶體操作的順序可能會與實際的操作執行順序不一致。由於現代的處理器都會使用寫緩衝區,因此它們都會允許對寫 – 讀操作執行重排序。

image-20221116215014247

❓ 如何解決重排序帶來的問題

對於編譯器,JMM的編譯器重排序規則會禁止特定類型的編譯器重排序(比如volatile)。

對於處理器重排序,JMM的處理器重排序規則會要求編譯器在生成指令序列時,插入特定類型的記憶體屏障(Memcry Barries /Memory Fence)指令,通過記憶體屏障指令來禁止特定類型的處理器重排序。
由於常見的處理器記憶體模型比JMM要弱, Java編譯器在生成位元組碼時,會在執行指令序列的適當位置插入記憶體屏障來限制處理器的重排序。同時,由於各種處理器記憶體模型的強弱不同,為了在不同的處理器平台向程式設計師展示一個一致的記憶體模型,JMM在不同的處理器中需要插入的記憶體屏障的數量和種類也不同。

CPU記憶體屏障:

  • LoadLoad: 禁止讀和讀的重排序;
  • StoreStore:禁止寫和寫的重排序;
  • LoadStore:禁止讀和寫的重排序;
  • StoreLoad:禁止寫和讀的重排序。

Java記憶體屏障

public final class Unsafe {
public native void loadFerice();// LoadLoad + LoadStore
public native void storeFence();// StoreStore + LoadStore
public native void fullFence(); // loadFence() + storeFence() + StoreLoad
}

❓ happens-before

JMM使用happens-before規則來闡述操作之間的記憶體可見性,以及什麼時候不能重排序。在JMM中。如果一個操作執行的結果需要對另一個操作可見,那麼這兩個操作之間必須要存在happens-before關係。換個角度來說.如果A happens-before B,則意味著A的執行結果必須對B可見,也就是保證跨執行緒的記憶體可見性。其中,前4條規則與程式設計師密切相關。

  • 1、程式順序規則:一個執行緒中的每個操作, happens-before於該執行緒中的任意後續操作;
  • 2、volatile變數規則:對一個volatile域的寫, happens-before於任意後續對這個volatile域的讀;
  • 3、synchronized規則:對一個鎖的解鎖, happens-before於隨後對這個鎖的加鎖;
  • 4、傳遞性:若A happens-before B,且B happens-before C,則A happens-before C;
  • 5、start()規則:若執行緒A執行Thread.start(),則執行緒A的start()操作happens-before於執行緒B中的任意操作;
  • 6、 join()規則︰若執行緒A執行ThreadB.join()並成功返回,那麼執行緒B中的任意操作happens-before於執行緒A從ThreadB.join()的成功返回。

image-20221116224120601

4、Volatile

🍊 volatile的基本特性

  • 可見性:對一個volatile變數的讀,總是能看到對這個volatile變數最後的寫入;
  • 原子性:對任意單個volatile變數的讀/寫具有原子性,但類似vclatile++這種複合操作不具有原子性。

🍐 volatile的記憶體語義

  • 寫記憶體語義:當寫一個volatile變數時,JMM會把該執行緒本地記憶體中的共享變數的值刷新到主記憶體;
  • 讀記憶體語義:當讀一個volatile變數時,JMM會把該執行緒本地記憶體置為無效,使其從主記憶體中讀取共享變數。

🥥 volatile的實現機制

為了實現volatile的記憶體語義,編譯器在生成位元組碼時,會在指令序列中插入記憶體屏障來禁止特定類型的處理器重排序。記憶體屏障插入策略非常保守,但它可以保證在任意處理器平台,任意的程式中都能得到正確的volatile記憶體語義。

  • 在每個volatile寫操作的前面插入一個StoreStore屏障;
  • 在每個volatile寫操作的後面插入一個StoreLoad屏障;
  • 在每個volatile讀操作的後面插入—個LoadLoad屏障;
  • 在每個volatile讀操作的後面插入一個LoadStore屏障。

🥩 volatile與鎖的對比

volatile僅僅保證對單個volatile變數的讀/寫具有原子性,而鎖的互斥執行的特性可以確保對整個臨界區程式碼的執行具有原子性。在功能上鎖比volatile更強大,在可伸縮性和執行性能上volatile更有優勢。

5、鎖

鎖的記憶體語義

  • 當執行緒釋放鎖時,JMM會把該執行緒對應的本地記憶體中的共享變數刷新到主記憶體中;
  • 當執行緒獲取鎖時,JMM會把該執行緒對應的本地記憶體置為無效。

鎖的實現機制

  • synchronized:採用CAS + Mark Word實現。存在鎖升級的情況;
  • Lock:採用CAS + volatile實現。存在鎖降級的情況核心是AQS 。

image-20221117130555447