😀 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