前置複習

../3.应用程序开发/多线程 Java SE/3.應用程式開發/多執行緒 > 實作 Callable 介面 THREAD 常用方法

本質都是 Thread 類別呼叫 start(),start()方法來啟動新執行緒

  1. 繼承 Thread 類別,改寫 run 方法
  2. 傳給 Thread 類別 runnable 實作類別,改寫 run 方法
  3. 建立 Callable(实现call方法)-->傳給FutureTask-->傳給Thread 也可直接 call.call():直接於目前執行緒中執行 Callable 任務(同步執行),這只是普通的方法呼叫,在目前執行緒中同步執行,不是多執行緒(解決傳統 Runnable 介面無法傳回值與擲出受檢異常的問題。)
  4. Executors 建立執行緒池

CompletableFuture 並不直接建立執行緒,它是對執行緒池(通常是 ForkJoinPool.commonPool())的封裝, 利用了 多執行緒(或者執行緒池)來實現非同步程式設計。

當使用 CompletableFuture 的非同步方法(例如 supplyAsync, runAsync)時,它會將任務提交給執行緒池執行

assets/JUC并发编程/file-20240909000006600.png

Java SE/3.應用程式開發/多執行緒 > 實作 Callable 介面

並發,concurrent) 同一 實體的多個事件,一個 處理器上同時處理多個任務 (科學運算、影像處理中的大規模資料平行處理 ) 平行,parallel): 不同 實體的多個事件,多個處理器處理多個任務(Web 伺服器處理多個用戶端請求) ../3.应用程序开发/assets/多线程/image-20231212213415636.png ../4.面对对象编程/3.应用程序开发/assets/多线程/image-20231212213420115.png

平行後並發

管程 : Monitor 監視器,也就是平時說的 鎖 JVM 靠 管程 來實現方法層級的同步與方法內部一段指令序列的同步

assets/JUC并发编程/image-20240415163028634.png

使用者執行緒 : 系統工作執行緒 守護執行緒 : 為其他執行緒服務,如 GC

isDaemon 判斷執行緒是使用者false還是守護 true assets/JUC并发编程/image-20240415164207070.png

CompletableFuture

CompletableFuture 並不直接建立執行緒,它是對執行緒池(通常是 ForkJoinPool.commonPool())的封裝, 利用了 使用多執行緒(或者執行緒池)來實現非同步程式設計。

CompletableFuture 是 Future 的增強版,減少阻塞和輪詢,可以傳入回呼物件,當非同步任務完成或者發生異常的時候,自動呼叫回呼物件的回呼方法

需求:多執行緒、能回傳,能非同步任務,普通執行緒無法滿足

Future 介面

FutureTask 是 Future 介面的一個具體實作類別,都代表一個非同步計算的結果

futureTask 對於結果的取得相關問題

get 存在阻塞問題 isDone() 方法容易消耗 CPU 資源


ExecutorService executor = Executors.newFixedThreadPool(1);
Future<Integer> future = executor.submit(() -> { Thread.sleep(2000); // 模擬一個耗時操作 return 42; })

../3.应用程序开发/assets/多线程/image-20240416105158341.png ../4.面对对象编程/3.应用程序开发/assets/多线程/image-20240416105237771.png

上面的輪詢方式並不優雅,還應該滿足以下需求

  1. 完成能夠主動告訴
  2. 能夠將不同的非同步結果組合
  3. 能夠選擇出執行最快的

assets/JUC并发编程/image-20240416105909440.png assets/JUC并发编程/image-20240416170938205.png

CompletionStage 介面

CompletionFuture 是 CompletionStage 介面和 Future 介面的一個具體實作類別,提供了更多的功能和操作方法來處理非同步計算任務。

assets/JUC并发编程/image-20240416174948778.png assets/JUC并发编程/image-20240416175221945.png

  1. 非同步任務 結束/出錯 時,會自動回呼某個物件的方法;
  2. 主執行緒設定好回呼後,不再關心非同步任務的執行,非同步任務之間可以順序執行
  • runAsync
  • supplyAsync

不推薦使用 CompatableFuture 的預設空參建立,因為這樣建立的 CompletableFuture 實例並沒有指定具體的非同步任務,可能會導致在後續使用時出現邏輯錯誤或異常。正確的做法是在建立 CompletableFuture 實例時傳入具體的非同步任務,以確保程式的正確性和穩定性 assets/JUC并发编程/image-20240416180038146.png

如果使用 runAsync,再 get 則還是之前的阻塞式

runAsync

assets/JUC并发编程/image-20240416184711733.png

執行緒池 類似於 守護執行緒 會在 Main 執行緒結束後立刻結束,因此要確保 main 執行緒最後結束

assets/JUC并发编程/image-20240416215142982.png

CompletableFuture

whenComplete


    public static void main(String[] args) throws ExecutionException, InterruptedException
    {
        ExecutorService threadPool = Executors.newFixedThreadPool(3);
        try
        {
            CompletableFuture.supplyAsync(
                    () -> {
                System.out.println(Thread.currentThread().getName() + "----come in");
                int result = ThreadLocalRandom.current().nextInt(10);
                try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
                System.out.println("-----1秒钟后出结果:" + result);
                if(result > 2)
                {
                    int i=10/0;
                }
                return result;
            },threadPool)
			        //v是上面的回傳值
                    .whenComplete((v,e) -> {
                if (e == null) {
                    System.out.println("-----计算完成,更新系统UpdateValue:"+v);
                }
            }).exceptionally(e -> {
                e.printStackTrace();
                System.out.println("异常情况:"+e.getCause()+"\t"+e.getMessage());
                return null;
            });

            System.out.println(Thread.currentThread().getName()+"线程先去忙其它任务");
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            threadPool.shutdown();
        }
        //主執行緒不要立刻結束,否則CompletableFuture預設使用的執行緒池會立刻關閉:暫停3秒鐘執行緒
        //try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
    }

電商網站比價開說

../新特性/JDK8-17新特性#2 函数式接口

assets/JUC并发编程/image-20240416222217500.png

轉換採用多執行緒案例,模擬查詢

將list中儲存的List<NetMall>取出轉化成List<CompletableFuture<String>>再轉換成List<String>

用到stream中的中間方法map Java SE/新特性/Stream流#中间方法

assets/JUC并发编程/image-20240416235732109.png 將NetMall物件轉換為一個CompletableFuture物件,並回傳一個代表非同步計算結果的CompletableFuture物件

不使用lambda的寫法

assets/JUC并发编程/image-20240708165443581.png


public static List<String> getPriceByCompletableFuture(List<NetMall> list,String productName)  
{  
    return list.stream().map(netMall -> CompletableFuture.supplyAsync(() -> String.format(productName + " in %s price is %.2f",netMall.getNetMallName(),netMall.calcPrice(productName)))).collect(Collectors.toList())//終結方法,將第一次計算的結果收集到一個列表  
            .stream()//再次呼叫stream()方法將列表轉換為一個新的流  
            .map(s -> s.join())//接著,使用map()方法對串流中的每個元素 s 進行轉換操作  
            .collect(Collectors.toList());//將所有的物件收集到一個列表中  
}

CompatableFuture 常用方法

  • runAsync
  • supplyAsync
  • get()
  • get()
1 獲得結果和觸發計算

assets/JUC并发编程/image-20240708211402813.png

2 對計算結果進行處理

thenApply、handle 兩者作用相同handle可以處理例外 assets/JUC并发编程/image-20240708213406427.png

assets/JUC并发编程/image-20240708214311150.png assets/JUC并发编程/image-20240708214418769.png

3 對計算結果進行消費

thenRun 不需要前一步的結果,會使用之前的執行緒池 thenRunAsync 非同步,會使用ForkJoinPool,不使用自訂執行緒池 thenAccept需要前一步結果,無傳回值,它傳回的是一個新的 CompletableFuture<Void> thenApply需要前一步結果,有傳回值 thenRunAsync 會使用ForkJoinPool,不使用自訂執行緒池

assets/JUC并发编程/image-20240708215422472.png join() 方法會阻塞目前執行緒,等待非同步工作執行完成。這樣可以確保在需要取得非同步工作結果的地方,能夠取得最終的結果

assets/JUC并发编程/image-20240709002304854.png

  • thenRunAsync

System.out.println(CompletableFuture.supplyAsync(() -> "resultA").thenRun(() -> {}).join());//null  
System.out.println(CompletableFuture.supplyAsync(() -> "resultA").thenAccept(r -> System.out.println(r)).join());//resultA和null,這個為thenAccept的傳回結果  
System.out.println(CompletableFuture.supplyAsync(() -> "resultA").thenAccept(r -> System.out.println(r)));  
//resultA和java.util.concurrent.CompletableFuture@76fb509a[Completed normally] 這個為thenAccept的傳回結果 ,此時由於沒有執行完沒有結果
System.out.println(CompletableFuture.supplyAsync(() -> "resultA").thenApply(r -> r + "resultB").join());//resultAresultB

4 對計算速度進行選用

applyToEither A.applyToEither(B, c -> { return c + " is winer"; });

CompletableFuture<String> playA = CompletableFuture.supplyAsync(() -> {  
    System.out.println("A come in");  
    try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }  
    return "playA";  
});  
  
CompletableFuture<String> playB = CompletableFuture.supplyAsync(() -> {  
    System.out.println("B come in");  
    try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }  
    return "playB";  
});  
  
CompletableFuture<String> result = playA.applyToEither(playB, f -> {  
    return f + " is winer";  
});  
  
System.out.println(Thread.currentThread().getName()+"\t"+"-----: "+result.join());
5 對計算結果進行合併

x.thenCombine(y,(x,y)->{})

兩個CompletionStage工作完成後,把兩個completableFuture的結果一起交給thenCombine來處理

CompletableFuture result = completableFuture1.thenCombine(completableFuture2, (x, y) -> {
System.out.println("-----开始两个结果合并");
return x + y;
}); assets/JUC并发编程/image-20240710201532281.png

assets/JUC并发编程/image-20240710201524670.png

鎖

assets/JUC并发编程/image-20240711002257100.png

assets/JUC并发编程/image-20240711002354882.png assets/JUC并发编程/image-20240711163939641.png

  • synchronized 方法 (鎖 this) assets/JUC并发编程/image-20240724102306730.png 一個物件裡面如果有多個 synchronized 方法,只要一個執行緒去呼叫其中的一個 synchronized 方法了,該物件其它的執行緒的 synchronized 方法都只能等待 也就是同一物件,只能有唯一的一個執行緒去存取這些 synchronized 方法

如果兩個物件,則不是同一把鎖,則各自呼叫各自的 synchronized 方法,不會有影響

  • static synchronized 方法 (鎖 類別範本檔案)

assets/JUC并发编程/image-20240724102316350.png

會加類別鎖,所有實例都會被鎖定

對於一般同步方法,鎖的是目前實例物件,通常指 this,具體的一支支手機,所有的一般同步方法用的都是同一把鎖>實例物件本身

對於靜態同步方法,鎖的是目前類別的Class物件,如Phone.class唯一的一個範本

assets/JUC并发编程/image-20240724001336532.png

synchronized

synchronized 同步程式碼區塊

位元組碼 monitorenter/monitorexit 來控制鎖的進出 一個 enter,兩個exit

如果有例外,為一個 enter,一個 exit assets/JUC并发编程/image-20240724010315394.png

synchronized 一般同步方法

assets/JUC并发编程/image-20240724010955528.png

synchronized 靜態同步方法

assets/JUC并发编程/image-20240724111755318.png

為什麼每個物件都可以成為一個鎖

在 Java 位元碼層面上,synchronized 關鍵字的實作依賴於monitorenter和monitorexit這兩個指令。每個 Java 物件都隱含地關聯著一個監視器鎖(monitor)。當執行monitorenter指令時,執行緒嘗試取得與物件關聯的監視器鎖的所有權。當執行monitorexit指令時,執行緒釋放該監視器鎖。

執行緒執行要求先持有管程

assets/JUC并发编程/image-20240724111909703.png

每個物件都天生帶一個物件監視器,每個被鎖住的物件都和 Monitor 關聯起來

其中_owner會指向持有鎖的執行緒,誰持有,誰記錄,誰釋放,誰取消

assets/JUC并发编程/image-20240724113655465.png assets/JUC并发编程/image-20240724113621705.png

ReentrantLock 公平和非公平鎖(預設)

公平:多個執行緒按照申請鎖的順序來取得鎖 非公平鎖:不按照申請鎖順序,可能後申請的執行緒比先申請的執行緒優先取得鎖,在高並發的環境下,有可能造成優先級反轉或鎖飢餓狀態

assets/JUC并发编程/image-20240724122115730.png assets/JUC并发编程/image-20240724122108011.png T

為什麼 ReentrantLock 預設非公平鎖
  1. 非公平鎖降低執行緒切換開銷
  2. 非公平鎖利用率高

assets/JUC并发编程/image-20240724122242084.png

公平非公平的選擇

非公平:吞吐量,節省時間 公平:雨露均霑

可重入鎖(遞迴鎖)

JAVA 微服務生態/Redis7/基礎/Redis 高級篇 > 可重用鎖

死鎖案例以及如何排除死鎖

互斥條件/不可搶奪/佔用且等待/循環等待

兩個執行緒互相想要對方的鎖 assets/JUC并发编程/image-20240725143942227.png

死鎖排查

jsp -l jstack process ID assets/JUC并发编程/image-20240725153538529.png

或者使用圖形化工具,例如 jconsole

小節

assets/JUC并发编程/image-20240725160544718.png

LockSupport 與執行緒中斷

assets/JUC并发编程/image-20240725162017815.png

執行緒中斷機制

interrupt/interrupted/isinterrupted

是協商機制,不是強制性,執行緒自行決定是否中斷 assets/JUC并发编程/image-20240725165338533.png interrupt 會設定中斷狀態為 True,並不能中斷執行緒,只是充當一個 flag,需要自己手動程式碼配合

如果執行緒本身處於 sleep,wait,join 狀態中,別的執行緒呼叫 interrupt 設定為 true,會拋出例外 interruptedException,例外會把 interrupt 狀態清除,變為 false,因此需要在例外處理的地方再次呼叫來確保執行緒終止

assets/JUC并发编程/image-20240726150418631.png

  • interrupted:回傳 current 執行緒的中斷狀態,然後重新把目前的狀態設定為 false

也就是假如中斷狀態為 True,呼叫 interrupted 回傳 current 執行緒的中斷狀態true,然後設定為false

exception interruptedException 發生後會把 interrupt 設定為 false,會導致其他執行緒設定的 true 失效

assets/JUC并发编程/image-20240726124021034.png

在沒有interrupt時,使用中斷標識中斷

在需要中斷的執行緒不斷監聽中斷狀態

  • volatile volatile,當volatile為 true 時中斷執行緒,然後在另外一個執行緒更改狀態

assets/JUC并发编程/image-20240725181449480.png

  • AtomicBoolean

assets/JUC并发编程/image-20240725181600676.png

  • interrupt

就是把對應執行緒的 interrupt 設定為 True,自己搭配 while 進行 break,可以達到上面程式碼類似的效果

底層都是透過 isInterrupted 實作

底層

assets/JUC并发编程/image-20240726161639723.png

LockSupport

LockSupport是用來建立鎖和其他同步類別的基本執行緒阻塞原語

執行緒等待喚醒機制

  • 三種讓執行緒等待和喚醒的方式 assets/JUC并发编程/image-20240726163807829.png assets/JUC并发编程/image-20240726185540020.png

Object 類別的wait/notify 必須在同步程式碼區塊或同步方法中成對出現

wait/notify

先wait後notify 必須在同步程式碼區塊或同步方法中成對出現

t1 ----come in t2 ----send notification t1 ----be awakened

t1 被喚醒後,會從 wait() 方法處嘗試重新取得 objectLock 的鎖

assets/JUC并发编程/image-20240726192724953.png

Java SE/3.應用程式開發/多執行緒 > new Condition

在這個範例中,awaitCondition 方法會使目前執行緒等待,直到 signalCondition 方法發出訊號

Lock condition | await/signal

先取得鎖Lock.lock() condition.await(); condition.signal();

assets/JUC并发编程/image-20240728113049875.png

Object和Condition 的限制是

  1. 必須先取得鎖或lock
  2. 必須先等待後再喚醒

LockSupport 解決了上面的限制 無需鎖區塊 支援先喚醒後等待(憑證設計)

先執行 unpark 會設定一個許可證,即使 park 在 unpark 後執行,已經有了許可證,直接進行放行,會消耗一個許可證

unpark 會增加一個許可證,不可以累計,最多只有一個,呼叫多次 unpark 也是只有一個許可證

LockSupport 中 park unpark

assets/JUC并发编程/image-20240728184425734.png assets/JUC并发编程/image-20240728113344520.png

assets/JUC并发编程/image-20240728191143764.png assets/JUC并发编程/image-20240728191209487.png

Java 記憶體模型 JMM

  • JMM 規範:JMM 規範控制著執行緒工作記憶體與主記憶體之間的資料同步。
  1. 何時將主記憶體中的資料同步到執行緒工作記憶體:
  2. 何時將執行緒工作記憶體中的資料刷新到主記憶體
  3. 提供 happens-before 關係: JMM 定義了一套 happens-before 規則,這些規則強制規定了某些操作之間的 happens-before 關係,

assets/JUC并发编程/image-20240729173848496.png

JVM 屏蔽掉各種硬體作業系統記憶體存取差異, JVM 本身是抽象的概念,描述一組規範,規範定義了程式中(尤其是多執行緒)各個變數的讀寫存取方式,並決定一個執行緒對共享變數的寫入何時以及如何變成對另一個執行緒可見,關鍵技術點都是圍繞多執行緒的 原子性、可見性和有序性展開的。

JVM 三大特性(可見、原子、有序)

可見性

可見性:一旦改變所有執行緒可見

java中的普通共享變數沒有保證可見性,也就是說主記憶體的共享變數沒有和共享變數副本進行同步,此時就需要將修改的資料同步到主記憶體過,且其他執行緒從主記憶體更新

主執行緒所做的修改也只是修改變數副本,並沒有直接刷新到主記憶體,也需要後續刷新

assets/JUC并发编程/image-20240729175607858.png assets/JUC并发编程/image-20240729180331580.png assets/JUC并发编程/image-20240729180835692.png

  • 原子性:一個操作不可以中斷,多執行緒下不能被其他執行緒干擾
  • 有序性 程式碼由上到下,有序執行
有序性

資料彼此沒有依賴,執行效果一致就可以重排

assets/JUC并发编程/image-20240729192829524.png assets/JUC并发编程/image-20240729214209368.png 語句四不可能被重排到第一個

Happens-before

就是將複雜的底層記憶體操作和處理器行為抽象成一組我們可以理解的規則(也就是底層是如何執行我們程式碼的),這個規則就是 HB 規則,只需要遵循這些 HB 規則就可以保證並行程式的正確性,而無需關心底層是如何實現的

如果你程式碼的執行不在 HB 規則中,就無法保證該程式碼執行時是否安全

assets/JUC并发编程/image-20240729233645537.png assets/JUC并发编程/image-20240729224950609.png

案例

assets/JUC并发编程/image-20240729234209464.png

解決,有兩種方式

assets/JUC并发编程/image-20240729234501464.png

assets/JUC并发编程/image-20240729234558829.png

volatile 與 Java 記憶體模型

assets/JUC并发编程/image-20240729235957345.png

寫 volatile 時,JMM 將該執行緒對應本地記憶體的共享變數值立刻刷新到主記憶體

讀 volatile 時,JMM 將 該執行緒對應本地記憶體的共享變數值 設定為無效,直接從主記憶體讀取共享變數

也就是 volatile 讀寫都是直接和主記憶體互動

assets/JUC并发编程/image-20240730120910418.png

記憶體屏障

volatile 底層實現透過記憶體屏障

記憶體屏障是一種屏障指令,它使得 CPU 或編譯器對屏障指令的前後所發出的記憶體操作,執行一個排序的約束,也叫記憶體柵欄或柵欄指令

記憶體屏障保證了 volatile 的可見性,只保證 volatile 變數

assets/JUC并发编程/image-20240731152649467.png

四大記憶體屏障插入策略分類

規律,XXYY XX先執行後再執行YY

assets/JUC并发编程/image-20240730153728934.png StoreLoad防止後續的 volatile 讀操作重排,但也間接的防止 volatile 寫操作(因為 volatile 寫前本身會再加StoreStore),可能間接地防止普通寫重排,也可能不防止

assets/JUC并发编程/JUC并发编程-20250313213423698.png

第一個操作和第二個操作代表程式碼中的先後順序

assets/JUC并发编程/image-20240730154300158.png

如果不禁止重排,那麼 volatile 讀操作之後的普通讀寫操作可能會被重排到 volatile 讀操作之前,從而破壞了 happens-before 關係,導致程式行為不可預測。 普通讀寫操作很可能依賴於 volatile 讀操作的結果

assets/JUC并发编程/image-20240730154313185.png

當第二個操作為volatile寫時,不論第一個操作是什麼,都不能重排序。這個操作保證了volatile寫之前的操作不會被重排到volatile寫之後

雖然通常說 StoreLoad 有防止後續的 volatile 讀或寫操作重排序到 volatile 寫操作之前的作用,但實際上它也間接起到了防止在前的普通讀操作被重排序到 volatile 寫之後的作用。

就算volatile寫前沒有加loadstore,JVM仍然可以透過以下方式保證程式正確 assets/JUC并发编程/JUC并发编程-20250313220141182.png

assets/JUC并发编程/image-20240730230231431.png

讀之後的操作不會重排到讀之前 寫之前的操作不會重排到寫之後

volatile寫:在每個volatile寫操作前或後插入一個StoreStotre/Storeload屏障 volatile Read在每個volatile Read操作後插入一個LoadLoad,LoadStore屏障

assets/JUC并发编程/image-20240730155435256.png assets/JUC并发编程/image-20240730155447112.png

volatile Write:在每個volatile Write操作前後插入一個StoreStotre/Storeload屏障
volatile Read在每個volatile Read操作後插入一個LoadLoad,LoadStore屏障

案例

volatile int v = 0;
int a = 0;

void example() {
    v = a; // volatile Write
    a = 1; // Normal Write
}

在這個例子中,有兩個操作:
1. `v = a;` 是一個 `volatile` 寫操作。
2. `a = 1;` 是一個普通寫操作。

根據表格中的規則:
1. `volatile` 寫操作之後的普通寫操作是可以重排的。
2. `volatile` 寫操作之後的 `volatile` 寫操作是不可以重排的。

**as-if-serial語意:** 無論怎麼重排序(編譯器和處理器為了提高並行度),單執行緒下的執行結果都不能被改變。
Happens-Before 規則的應用: 儘管 a = 1; 發生在 v = a; 之後,但根據資料依賴性,為了得到正確的v,實際上 a 的賦值 happens-before v = a。
總結:

- 儘管 JMM 允許 `volatile` 寫操作之後的普通寫操作重排,但是在存在資料依賴關係的情況下,JMM 會阻止這種重排序。

volatile 特性

JVM 自身只能保證單個命令的原子性,多個命令只能靠鎖來保證

volatile 不能保證原子性,只能保證可見性 保證多個命令的原子性需要依靠 鎖,鎖機制提供了可見性和原子性,可以代替 volatile

volotile 不適合參與到依賴當前值的運算

可見性

volatile 保證可見性

assets/JUC并发编程/image-20240730160705429.png 不加volatile程式不會停止,主執行緒做出的修改也是修改變數副本,沒有刷新到主記憶體,也有可能刷到了主記憶體,但另一個執行緒使用的還是副本,沒有從主記憶體取得最新值

volatile變數的讀寫過程

assets/JUC并发编程/image-20240730171811625.png assets/JUC并发编程/image-20240730171819869.png assets/JUC并发编程/image-20240730193348187.png

沒有原子性

不保證原子性操作

只能保證可見性,從主記憶體直接讀取與儲存資料,而多執行緒的操作同樣可能出錯 原子性需要lock

例如 i++ assets/JUC并发编程/image-20240730160321120.png assets/JUC并发编程/image-20240730184655305.png 正確結果 10000,測試結果 8177

根據上面的變數讀寫過程

assets/JUC并发编程/image-20240730193419295.png assets/JUC并发编程/image-20240730193434489.png assets/JUC并发编程/image-20240730193447727.png

  • 原子性:關注的是一個 操作(可能包含多個步驟)是否作為一個整體執行,不被其他執行緒干擾。
  • 禁止重排序(volatile):關注的是 單一變數 的讀寫操作之間的順序,以及與其他 volatile 變數讀寫操作的順序。

指令禁重排

volatile

禁止 寫入之前 的操作重排到 volatile 之後(這意味著在 volatile 寫入操作之前的所有寫入操作都已經 完成 並對其他執行緒可見),但寫入之後的操作可能會被重排,此時如果有影響則需要上鎖

也就是說 volatile 讀取 操作 之後 的操作不會重排到讀取之前,寫入 操作 之前 的操作一定已經完成,而 鎖 可以直接避免所有指令重排

volatile 讀取 之後 加 LoadStore 和 LoadLoad volatile 寫入 之前 加 StoreStore,後加 Storeload 讀取之後的操作不會重排到讀取之前 寫入之前的操作不會重排到寫入之後

volatile禁重排

assets/JUC并发编程/image-20240731155507442.png assets/JUC并发编程/image-20240731155513507.png assets/JUC并发编程/image-20240731155524177.png assets/JUC并发编程/image-20240731155545392.png assets/JUC并发编程/image-20240731161639195.png assets/JUC并发编程/image-20240730154300158.png

assets/JUC并发编程/image-20240731162038037.png

寫入之前的操作重排到 volatile 之後(這意味著在 volatile 寫入操作之前的所有寫入操作都已經完成並對其他執行緒可見),但寫入之後的操作可能會被重排,此時如果有影響則需要上鎖

在 Java 記憶體模型中,普通的寫入操作(如 y = 1)是可以被重排序到 volatile 寫入操作之前的。這確實可能影響 volatile 變數的預期行為,導致在其他執行緒中讀取到的值不一致。

案例

int x = 0; volatile int v = 0; int y = 0;

// Thread A x = 1; // 普通寫入操作 v = y; // volatile 寫入操作 y = 1; // 普通寫入操作

執行緒A中的操作順序

在Thread A中,我們有三步操作:

  1. x = 1; (普通寫入操作)
  2. v = y; (volatile寫入操作)
  3. y = 1; (普通寫入操作)

重排序規則

根據Java記憶體模型,volatile寫入操作會在其前後插入記憶體屏障:

  1. 寫屏障(Store Barrier):在寫入volatile變數之前,確保所有之前的普通寫入操作都已經完成並對其他執行緒可見。
  2. 讀屏障(Load Barrier):在讀取volatile變數之後,確保所有後續的普通讀取操作讀取到最新的值。

針對你的問題:普通寫入操作是否會被重排到volatile寫入操作之前

在Thread A中:

  • x = 1 是普通寫入操作。
  • v = y 是volatile寫入操作。
  • y = 1 是普通寫入操作。

根據記憶體模型的規則:

  • volatile寫入操作之前的所有普通寫入操作不能被重排到volatile寫入操作之後。
  • 但是,volatile寫入操作之後的普通寫入操作是可以被重排到volatile寫入操作之前的。

具體範例

考慮以下程式碼:

x = 1; / 普通寫入操作 v = y; / volatile 寫入操作 y = 1; / 普通寫入操作

在這種情況下,可能會發生以下重排序:

  • x = 1 和 v = y 的順序是固定的,x = 1 必須在 v = y 之前完成。
  • 但是 y = 1 可能會被重排序到 v = y 之前。

因此,可能的執行順序包括:

  1. x = 1
  2. y = 1
  3. v = y

對執行緒B的影響

對於Thread B:

int r1 = v; / volatile 讀取操作 int r2 = x; / 普通讀取操作 int r3 = y; / 普通讀取操作

如果r1 = y,那麼根據記憶體模型的保證,r2的值必定是1(因為x = 1在v = y之前完成)。但是,r3的值不確定,因為y = 1可能在v = y之前,也可能在v = y之後。

使用方式

當作狀態標識

assets/JUC并发编程/image-20240731181850358.png

讀 : volitile 保證可見性 寫 : 鎖保證原子性

開銷較低的讀,寫鎖策略

有很多讀,降低開銷,不用加鎖,只需要保證可見性即可

assets/JUC并发编程/image-20240731181902112.png

多執行緒使用 volatile 禁用reordering避免問題

物件建立分為三步,多執行緒可能發出重排導致出錯,因此可以使用 volatile 禁用 reordering 避免問題

DCL 雙端鎖

雙檢

assets/JUC并发编程/image-20240731182843038.png

第二次加鎖因為:可能別的執行緒在開始檢查時沒有建立完成,而檢查鎖時正好建立完成釋放了鎖

由於SafeDoubleCheckSingleton沒有加volatile singleton = new SafeDoubleCheckSingleton(); 這行程式碼實際上分為三個步驟:

  1. 為 SafeDoubleCheckSingleton 分配記憶體空間。
  2. 呼叫 SafeDoubleCheckSingleton 的建構函式來初始化物件。
  3. 將 singleton 參照指向分配的記憶體位址。

由於重排序,可能進行到第二步,物件還沒有建立完畢,另一個執行緒進行判斷以為物件已經建立完成,導致出錯,因此需要volatile保證不會發生重排

assets/JUC并发编程/image-20240731190657168.png

總結

可見性 assets/JUC并发编程/image-20240731192632872.png

原子性

禁重排

assets/JUC并发编程/image-20240731231332029.png

assets/JUC并发编程/image-20240731231409428.png

assets/JUC并发编程/image-20240731231350410.png

為什麼寫了 volatile 後系統底層就加入了記憶體屏障

assets/JUC并发编程/image-20240731221823179.png

CAS 比較並交換

CAS 的全稱為 Compare-And-Swap,它是一條CPU並發原語。原語的執行必須是連續的,在執行過程中不允許被中斷,也就是說 CAS 是一條 CPU 的原子指令,不會造成所謂的資料不一致問題

它的功能是判斷記憶體某個位置的值是否為預期值,如果是則更改為新的值,這個過程是原子的。採用樂觀鎖重試機制

AtomicInteger 類主要利用CAS (compare and swap) + volatile和 native 方法來保證原子操作,從而避免 synchronized的高開銷,執行效率大為提升

沒有CAS時 : 多執行緒需要加 鎖 來保證原子性 有 CAS 時 : 多執行緒使用原子類可以保證原子性

但是存在ABA 問題

CAS 是什麼

assets/JUC并发编程/image-20240801124819665.png assets/JUC并发编程/image-20240801124912506.png 如果主記憶體是 5,多執行緒共享變數副本是 5,更新 6 後準備寫入主記憶體時,會檢查共享變數副本是否依舊和主記憶體相等,相等則認為沒有別的執行緒修改,否則將不更新,重新讀取主記憶體的值

  • 硬體級別保證

assets/JUC并发编程/image-20240801130518272.png 它透過硬體級別的指令(如 cmpxchg 指令)來確保比較-更新操作的原子性。這樣可以避免傳統鎖機制帶來的執行緒阻塞和上下文切換開銷,從而提高系統的效率和回應速度。

assets/JUC并发编程/image-20240801132044055.png

原始碼分析

CAS 是一條 CPU 的原子指令,CPU 並發原句,不會造成所謂的資料不一致問題

Unsafe 是 CAS 的核心類,可以直接呼叫作業系統底層

原始碼分析

assets/JUC并发编程/image-20240801144030860.png assets/JUC并发编程/image-20240801144035086.png assets/JUC并发编程/image-20240801144046650.png assets/JUC并发编程/image-20240801144053650.png assets/JUC并发编程/image-20240801144243872.png assets/JUC并发编程/image-20240801144326601.png

自旋

嘗試獲取鎖的執行緒不會立即阻塞,而是採用迴圈的方式去嘗試獲取鎖,

當執行緒發現鎖被佔用時,會不斷迴圈判斷鎖的狀態,直到獲取。這樣的好處是減少執行緒上下文切換的消耗,缺點是迴圈會消耗CPU

java

package com.atguigu.Interview.study.thread;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;

/**
 * @auther zzyy
 * @create 2018-12-28 17:57
 * 題目:實作一個自旋鎖
 * Spin lock advantage: looping comparison to acquire without similar wait blocking.
 *
 * Through CAS operation completes the spin lock,A the thread that comes in first calls myLock method and holds the lock itself 5 seconds,B then after coming in finds
 * currently a thread holds the lock, not null,so it can only wait by spinning, until A after the lock is released B then grabs it.
 */
public class SpinLockDemo
{
    AtomicReference<Thread> atomicReference = new AtomicReference<>();

    public void myLock()
    {
        Thread thread = Thread.currentThread();
        System.out.println(Thread.currentThread().getName()+"\t come in");
        while(!atomicReference.compareAndSet(null,thread))
        {

        }
    }

    public void myUnLock()
    {
        Thread thread = Thread.currentThread();
        atomicReference.compareAndSet(thread,null);
        System.out.println(Thread.currentThread().getName()+"\t myUnLock over");
    }

    public static void main(String[] args)
    {
        SpinLockDemo spinLockDemo = new SpinLockDemo();

        new Thread(() -> {
            spinLockDemo.myLock();
            try { TimeUnit.SECONDS.sleep( 5 ); } catch (InterruptedException e) { e.printStackTrace(); }
            spinLockDemo.myUnLock();
        },"A").start();


        //Pause the thread for a while, to ensure A the thread runs before B the thread starts and completes
        try { TimeUnit.SECONDS.sleep( 1 ); } catch (InterruptedException e) { e.printStackTrace(); }

        new Thread(() -> {
            spinLockDemo.myLock();
            spinLockDemo.myUnLock();
        },"B").start();

    }
}

CAS 問題

  • 迴圈開銷可能大 assets/JUC并发编程/image-20240801162129327.png
ABA 問題

執行緒 A 修改記憶體中的資料時,執行緒 B 過來使用了資料,B 改變資料後,又將資料恢復成原樣,對於執行緒 A 檢查後以為沒有變過而修改成功,出現了過程不安全問題 assets/JUC并发编程/image-20240801164038389.png 因此引用了版本號機制, AtomicStampedReference assets/JUC并发编程/image-20240801163909953.png assets/JUC并发编程/image-20240801173730690.png

原子操作類之 18 羅漢增強

當執行緒數量增多時,使用原子操作類的優勢會更加明顯

assets/JUC并发编程/image-20240801174406785.png assets/JUC并发编程/image-20240801174356799.png

基本型別原子類

AtomicInteger/AtomicBoolean/AtomicLong

class MyNumber
{
    @Getter
    private AtomicInteger atomicInteger = new AtomicInteger();//The default is 0
    public void addPlusPlus()
    {
        atomicInteger.incrementAndGet();
    }
}

assets/JUC并发编程/image-20240801183702223.png

Java SE/3.應用程式開發/多執行緒 > CountDownLatch

陣列型別原子類

AtomiclntegerArray AtomicLongArray AtomicReferenceArray

assets/JUC并发编程/image-20240801190300630.png

參照型別原子類

AtomicReference AtomicStampedReference 版本號 AtomicMarkableReference

原子參照

new AtomicReference<>();

assets/JUC并发编程/image-20240801153957395.png

將之前設置的z3轉換成li4

AtomicMarkableReference

AtomicMarkableReference,它允許你原子性地更新一個參照和一個boolean標記位。

true/false的更改

assets/JUC并发编程/image-20240802003211363.png

assets/JUC并发编程/image-20240802003130179.png

物件的屬性修改原子類

可以以 thread safe way 操作non thread safe object內的某些欄位

欄位級別原子更新

AtomicIntegerFieldUpdater 原子更新物件中 int 型別欄位的值 AtomicLongFieldUpdater 原子更新物件中 Long 型別欄位的值 AtomicReferenceFieldUpdater 原子更新reference type欄位的值 都必須使用 public volatile 關鍵字修飾

因為物件的屬性修改型別原子類都是抽象類,所以每次使用都必須使用靜態方法 newUpdater() 建立一個更新器,並且需要設定想要更新的類和屬性。

不能避免 ABA,不加synchronized,保證高效能原子性

AtomicIntegerFieldUpdater

class BankAccount//resource class
{
    String bankName = "CCB";
    //更新的物件屬性必須使用 public volatile 修飾符。
    public volatile int money = 0;//錢數
    public void add()
    {
        money++;
    }
    //因為物件的屬性修改類型原子類都是抽象類,所以每次使用都必須
    //使用靜態方法newUpdater()建立一個更新器,並且需要設定想要更新的類和屬性
    AtomicIntegerFieldUpdater<BankAccount> fieldUpdater =
            AtomicIntegerFieldUpdater.newUpdater(BankAccount.class,"money");

    //不加synchronized,保證高效能原子性,局部微創小手術
    public void transMoney(BankAccount bankAccount)
    {
        fieldUpdater.getAndIncrement(bankAccount);
    }
}

/**
 * @auther zzyy
 * 以一種執行緒安全的方式操作非執行緒安全物件的某些欄位。
 *
 * 需求:
 * 10個執行緒,
 * 每個執行緒轉帳1000,
 * 不使用synchronized,嘗試使用AtomicIntegerFieldUpdater來實作。
 */

public class AtomicIntegerFieldUpdaterDemo
{
    public static void main(String[] args) throws InterruptedException
    {
        BankAccount bankAccount = new BankAccount();
        CountDownLatch countDownLatch = new CountDownLatch(10);

        for (int i = 1; i <=10; i++) {
            new Thread(() -> {
                try {
                    for (int j = 1; j <=1000; j++) {
                        //bankAccount.add();
                        bankAccount.transMoney(bankAccount);
                    }
                } finally {
                    countDownLatch.countDown();
                }
            },String.valueOf(i)).start();
        }
        countDownLatch.await();

        System.out.println(Thread.currentThread().getName()+"\t"+"result: "+bankAccount.money);
    }
}

AtomicReferenceFieldUpdater.newUpdater(当前类,當前參數類,參數名) 對類中欄位更新 referenceFieldUpdater.compareAndSet(myVar,Boolean.FALSE,Boolean.TRUE)

AtomicReferenceFieldUpdater

class MyVar //資源類
{
    public volatile Boolean isInit = Boolean.FALSE;

    AtomicReferenceFieldUpdater<MyVar,Boolean> referenceFieldUpdater =
            AtomicReferenceFieldUpdater.newUpdater(MyVar.class,Boolean.class,"isInit");

    public void init(MyVar myVar)
    {
        if (referenceFieldUpdater.compareAndSet(myVar,Boolean.FALSE,Boolean.TRUE))
        {
            System.out.println(Thread.currentThread().getName()+"\t"+"----- start init,need 2 seconds");
            //暫停幾秒鐘執行緒
            try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
            System.out.println(Thread.currentThread().getName()+"\t"+"----- over init");
        }else{
            System.out.println(Thread.currentThread().getName()+"\t"+"----- 已经有线程在进行初始化工作。。。。。");
        }
    }
}


/**
 * @auther zzyy
 * 需求:
 * 多執行緒並行呼叫一個類的初始化方法,如果未被初始化過,將執行初始化工作,
 * 要求只能被初始化一次,只有一個執行緒操作成功
 */
 
public class AtomicReferenceFieldUpdaterDemo
{
    public static void main(String[] args)
    {
        MyVar myVar = new MyVar();

        for (int i = 1; i <=5; i++) {
            new Thread(() -> {
                myVar.init(myVar);
            },String.valueOf(i)).start();
        }
    }
}

原子操作增強類原理深度解析

為什麼出現又增強了什麼

assets/JUC并发编程/image-20240802224405044.png assets/JUC并发编程/image-20240802224558080.png assets/JUC并发编程/image-20240802224536756.png

DoubleAccumulator 建立一個初始總和 0 的新加法器,之後和每次傳入的新數做運輸,取決於重寫 applyAsLong 的邏輯 DoubleAdder LongAccumulator LongAdder

assets/JUC并发编程/image-20240803005418858.png

DoubleAdder,DoubleAccumulator 和效能對比

assets/JUC并发编程/image-20240802232939710.png

對比測試,效能相差 10 倍 assets/JUC并发编程/JUC并发编程-20250316181436884.png


import javax.lang.model.element.VariableElement;
import java.util.HashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.LongAccumulator;
import java.util.concurrent.atomic.LongAdder;

class ClickNumber //資源類
    //四種計算方式,測試效能
{
    int number = 0;
    public synchronized void clickBySynchronized()
    {
        number++;
    }

    AtomicLong atomicLong = new AtomicLong(0);
    public void clickByAtomicLong()
    {
        atomicLong.getAndIncrement();
    }

    LongAdder longAdder = new LongAdder();
    public void clickByLongAdder()
    {
        longAdder.increment();
    }

    LongAccumulator longAccumulator = new LongAccumulator((x,y) -> x + y,0);
    public void clickByLongAccumulator()
    {
        longAccumulator.accumulate(1);
    }

}

/**
 * @auther zzyy
 * 需求: 50個執行緒,每個執行緒100W次,總點讚數出來
 */
public class AccumulatorCompareDemo
{
    public static final int _1W = 10000;
    public static final int threadNumber = 50;

    public static void main(String[] args) throws InterruptedException
    {
        ClickNumber clickNumber = new ClickNumber();
        long startTime;
        long endTime;

        CountDownLatch countDownLatch1 = new CountDownLatch(threadNumber);
        CountDownLatch countDownLatch2 = new CountDownLatch(threadNumber);
        CountDownLatch countDownLatch3 = new CountDownLatch(threadNumber);
        CountDownLatch countDownLatch4 = new CountDownLatch(threadNumber);

        startTime = System.currentTimeMillis();
        for (int i = 1; i <=threadNumber; i++) {
            new Thread(() -> {
                try {
                    for (int j = 1; j <=100 * _1W; j++) {
                        clickNumber.clickBySynchronized();
                    }
                } finally {
                    countDownLatch1.countDown();
                }
            },String.valueOf(i)).start();
        }
        countDownLatch1.await();
        endTime = System.currentTimeMillis();
        System.out.println("----costTime: "+(endTime - startTime) +" 毫秒"+"\t clickBySynchronized: "+clickNumber.number);

        startTime = System.currentTimeMillis();
        for (int i = 1; i <=threadNumber; i++) {
            new Thread(() -> {
                try {
                    for (int j = 1; j <=100 * _1W; j++) {
                        clickNumber.clickByAtomicLong();
                    }
                } finally {
                    countDownLatch2.countDown();
                }
            },String.valueOf(i)).start();
        }
        countDownLatch2.await();
        endTime = System.currentTimeMillis();
        System.out.println("----costTime: "+(endTime - startTime) +" 毫秒"+"\t clickByAtomicLong: "+clickNumber.atomicLong.get());


        startTime = System.currentTimeMillis();
        for (int i = 1; i <=threadNumber; i++) {
            new Thread(() -> {
                try {
                    for (int j = 1; j <=100 * _1W; j++) {
                        clickNumber.clickByLongAdder();
                    }
                } finally {
                    countDownLatch3.countDown();
                }
            },String.valueOf(i)).start();
        }
        countDownLatch3.await();
        endTime = System.currentTimeMillis();
        System.out.println("----costTime: "+(endTime - startTime) +" 毫秒"+"\t clickByLongAdder: "+clickNumber.longAdder.sum());

        startTime = System.currentTimeMillis();
        for (int i = 1; i <=threadNumber; i++) {
            new Thread(() -> {
                try {
                    for (int j = 1; j <=100 * _1W; j++) {
                        clickNumber.clickByLongAccumulator();
                    }
                } finally {
                    countDownLatch4.countDown();
                }
            },String.valueOf(i)).start();
        }
        countDownLatch4.await();
        endTime = System.currentTimeMillis();
        System.out.println("----costTime: "+(endTime - startTime) +" 毫秒"+"\t clickByLongAccumulator: "+clickNumber.longAccumulator.get());

    }
}

底層原始碼

CAS 只允許一個執行緒成功修改共享變數的值,其餘執行緒會失敗並重試,當並行量高時會出現非常多的重試,而 LongAdder 內部維護了一個基於 CAS 的 cell 陣列。每個 cell 獨立維護一個計數值。會對每個 cell 都可以有一個執行緒進入然後進行更新,多個執行緒可以操作不同的 cell,最後只需要將每個 cell 累加,得到結果

每個執行緒 ID 進行 hash,hash 映射到某個陣列 cell 的下標,然後再對該下標的值進行自增操作

LongAdder 為何快

LongAdder是Striped64的子類

Striped64原始碼

assets/JUC并发编程/image-20240803120050342.png

CAS只允許一個執行緒成功修改共享變數的值,其餘執行緒會失敗並重試,當並行量高時會出現非常多的重試,而LongAdder內部維護了一個基於CAS的cell陣列。每個cell獨立維護一個計數值。會對每個cell都可以有一個執行緒進入然後進行更新,多個執行緒可以操作不同的cell,最後只需要將每個cell累加,得到結果 assets/JUC并发编程/image-20240803141847820.png LongAdder等於 base 變數加 Cell[]陣列 assets/JUC并发编程/image-20240803123511690.png

assets/JUC并发编程/image-20240803123458955.png

assets/JUC并发编程/image-20240803123728656.png

assets/JUC并发编程/image-20240803135335269.png assets/JUC并发编程/image-20240803145927792.png

https://www.bilibili.com/video/BV1ar4y1x727/?p=90&vd_source=17535a06cd4587318e9b67ac64afeab7

92 到 99 沒看

LongAdder原始碼

assets/JUC并发编程/image-20240803154305457.png (as 等於 cells) 不等於 null : 檢查 cells 陣列是否已初始化,初始化為ture 未初始化則進行,!casBase(b = base, b + x),嘗試使用 CAS 操作直接更新基礎計數器 base。

assets/JUC并发编程/image-20240803153138816.png

一個 base,然後新建兩個 cell

鎖-CAS-Volatile 總結

JMM: 可見性,原子性,有序 (指令重排)

可見性 原子性 有序性
Volatile √ × 部分
CAS × √ ×
Volatile+CAS √ √ 部分
鎖 √ √ √

加鎖,即使操作的是普通的、非 volatile 變數,也能夠保證可見性、原子性和有序性。 並且,在這種情況下,確實不需要再額外使用 volatile 關鍵字

  • Volatile

Volatile 提供了可見性, 部分 有序性,不保證原子性

讀之後的操作不會重排到讀之前 寫之前的操作不會重排到寫之後

讀 : volitile 保證可見性 寫 : 鎖保證原子性

不能保證多個 volatile 變數之間的順序

volatile int x = 0;
volatile int y = 0;

// 執行緒1
x = 1;
y = 2;

// 執行緒2
System.out.println("x = " + x + ", y = " + y);

volatile int a = 0;
int b = 0;

//執行緒1
a = 1;
b = 2;

//執行緒2
if (b == 2) {
    System.out.println(a); // 可能輸出0,也可能輸出1
}
  • CAS Compare-And-Swap

各種原子類,採用樂觀鎖機制 透過硬體級別的指令(如 cmpxchg 指令)來確保比較-更新操作的原子性 通常配合 volatile 實現 可見性,和 部分有序性

assets/JUC并发编程/image-20240822130345742.png

沒有 CAS 時 : 多執行緒需要加 鎖 來保證原子性 有 CAS 時 : 多執行緒使用 原子類 可以保證原子性

  • 鎖偏向鎖-輕量-重量

鎖可以直接保證 可見,原子,有序 ,無需volatile

通常情況下,如果並發讀寫操作較多且競爭不激烈,可以考慮使用 CAS 搭配 volatile;如果需要更強的執行緒安全性或者競爭比較激烈,可以選擇直接使用鎖。

ThreadLocal 提供了一種執行緒 區域變數,每個執行緒都有一份,再也不需要使用 Voliatile/CAS/鎖

ThreadLocal

ThreadLocal 提供執行緒區域變數。這些變數與正常的變數不同,因為每一個執行緒在存取 ThreadLocal 實例的時候(透過其 get 或 set 方法)都有自己的、獨立初始化的變數副本。ThreadLocal 實例通常是類別中的私有靜態欄位,使用它的目的是希望將狀態(例如,使用者 ID 或交易 ID)與執行緒關聯起來。

實現每一個執行緒都有自己專屬的本地變數副本 (自己用自己的變數不麻煩別人,不和其他人共享,人人有份,人各一份)

提供了一種執行緒 區域變數,每個執行緒都有一份,再也不需要使用 Voliatile/CAS/鎖

使用方式

assets/JUC并发编程/image-20240803222847750.png

  • 初始化方式 initialValue/withInitial 兩種方式一致 withInitial 是新的方式,比前者簡潔

initialValue 初始化複雜

private static final ThreadLocal<Integer> saleVolume = new ThreadLocal<>() {
    @Override
    protected Integer initialValue() {
        return 0;
    }
};

public static ThreadLocal<Long> threadLocal = new ThreadLocal<>();不建議直接初始化 預設為 null 可能報空指標異常

建議使用 withInitial 初始化,因為ThreadLocal<Integer> saleVolume = ThreadLocal.withInitial(() -> 0);只需要初始化一次,之後每個執行緒使用都是有 一份 自己的初始值,之後可以操作這個資料,但每個執行緒只能有一份,每個執行緒自己用完了需要使用 remove() 進行回收

使用完後需要回收自訂的 ThreadLocal 變數,通常使用try-catch-finally

並不在於 ThreadLocal 實現了 ThreadLocal, 而在於 ThreadLocalMap,每個執行緒操作 ThreadLocal 中的 set 方法,都會和自己當前的執行緒進行綁定 , 所以 ThreadLocal 只需要初始化一次,只分配一塊空間就夠了,所以經常使用 static 修飾

assets/JUC并发编程/image-20240804123057398.png

assets/JUC并发编程/image-20240803223100513.png

java

案例測試每個執行緒記錄自己的資料


import java.util.Random;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;

class House //資源類
{
    int saleCount = 0;
    public synchronized void saleHouse()
    {
        ++saleCount;
    }

    ThreadLocal<Integer> saleVolume = ThreadLocal.withInitial(() -> 0);
    public void saleVolumeByThreadLocal()
    {
        saleVolume.set(1+saleVolume.get());
    }
}

/**
 * @auther zzyy
 * @create 2021-12-31 15:46
 * 需求1: 5個銷售賣房子,集團高層只關心銷售總量的準確統計數。
 * 需求2: 5個銷售賣完隨機數房子,各自獨立銷售額度,自己業績按提成走,分灶吃飯,各個銷售自己動手,豐衣足食
 */

public class ThreadLocalDemo
{
    public static void main(String[] args) throws InterruptedException
    {

        House house = new House();

        for (int i = 1; i <=5; i++) {
            new Thread(() -> {
                int size = new Random().nextInt(5)+1;
                try {
                    for (int j = 1; j <=size; j++) {
                        house.saleHouse();
                        house.saleVolumeByThreadLocal();
                    }
                    System.out.println(Thread.currentThread().getName()+"\t"+"号销售卖出:"+house.saleVolume.get());
                } finally {
                    house.saleVolume.remove();
                }
            },String.valueOf(i)).start();
        };

        //暫停毫秒
        try { TimeUnit.MILLISECONDS.sleep(300); } catch (InterruptedException e) { e.printStackTrace(); }

        System.out.println(Thread.currentThread().getName()+"\t"+"共计卖出多少套: "+house.saleCount);
    }
}

原始碼分析

Thread--> ThreadLocal -->ThreadLocalMap-->當前執行緒和當前變數

Thread 包含 ThreadLocal ThreadLocal 包含靜態 ThreadLocalMap 類別 ThreadLocalMap 包含儲存當前執行緒和當前變數

多個執行緒可以同時存取的其實是同一個 ThreadLocal 實例,但每個執行緒都有自己的 ThreadLocalMap 物件(threadLocals) 每個執行緒都有自己的 Threadlocals 變數,是一個 ThreadlocalMap 集合,ThreadLocal 就是幫助我們把資料放到自己的 Threadlocals 裡

類似 : 人: 卡片 : 卡片資訊的關係 assets/JUC并发编程/image-20240804154343938.png assets/JUC并发编程/image-20240804161428479.png assets/JUC并发编程/image-20240822195901488.png

記憶體洩漏問題

ThreadLocalMap 是經過兩層包裝的 ThreadLocal 物件

(1)第一層包裝是使用 WeakReference<ThreadLocal<?>>將ThreadLocal物件變成一個弱引用的物件(藍框) (2)第二層包裝是定義了一個專門的類別 Entry 來擴充WeakReference<ThreadLocal<?>>

assets/JUC并发编程/image-20240804173652406.png assets/JUC并发编程/image-20240804174507631.png assets/JUC并发编程/image-20240804174641475.png

JVM 上記憶體與垃圾回收 > 再談引用

一個執行緒可以使用多個 ThreadLocal 變數,所以 key 使用 ThreadLocal,用於區分同一個執行緒的 ThreadLocalMap 中儲存的不同執行緒區域變數

引用複習

軟引用 通常用於對記憶體敏感的程式中,例如快取就有用到軟引用,記憶體夠用時就保留,不夠用就回收 assets/JUC并发编程/image-20240804180530352.png

弱引用: 發現就回收 assets/JUC并发编程/image-20240804180804388.png

虛引用: 顧名思義,就是形同虛設,與其他幾種引用都不同,虛引用並不會決定物件的生命週期。 assets/JUC并发编程/image-20240804180707056.png 設定虛引用關聯的唯一目的,就是在這個物件被收集器回收時收到一個系統通知,或者後續新增進一步的處理。

執行緒執行完後 Threadlocal 應該被回收,而 ThreadLocalMap 中儲存 ThreadLocal 的值,也就是引用了 ThreadLocal , 如果是強引用,就會導致 Threadlocal 沒有被回收,造成記憶體洩漏

Entry 弱引用 ThreadLocal

Entry extends WeakReference<ThreadLocal<?>>

因此需要設計成弱引用,但此時仍不完善 設計成弱引用後, ThreadLocal被回收後 Entry 的 key 就會指向 null, 而value 的值是強引用Object value;,且因為設定為 null 後該鍵對值無法存取,造成了記憶體洩漏, 因此弱引用不能 100% 保證記憶體不洩漏 , 所以需要手動清理 remove, set/get 也會檢查所有鍵為 null 的 Entry 物件

當然在執行緒結束的時候 Thread 停止會被回收,但是執行緒池會重複使用執行緒,執行緒是不會停止的,此時洩漏就需要注意

A 類別弱引用類別 B : A extends WeakReference<B>

強引用鏈 Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value

為什麼原始碼用弱引用

assets/JUC并发编程/image-20240804173652406.png Entry 擴充了 WeakReference<ThreadLocal<?>>,這意味著它持有一個 ThreadLocal<?> 物件的弱引用

assets/JUC并发编程/image-20240804182844169.png assets/JUC并发编程/image-20240804184646569.png 使用弱引用回收後 Entry 的 key 就會指向 null

由於在執行緒池情境下,執行緒池經常被重複使用 Object value; 此時僅僅使用弱引用,就會存在很多key為ull的entry,但value一直存在也會再引發新的記憶體洩漏,因此需要remove手動清理

值是強引用的主要原因,是為了確保執行緒區域變數的值在鍵存在的情況下可以被正常存取。如果值也是弱引用,那麼在垃圾回收時,即使鍵還存在,值也可能被回收,這樣會導致意外的行為。

assets/JUC并发编程/image-20240804185058817.png assets/JUC并发编程/image-20240804185902776.png assets/JUC并发编程/image-20240804193323577.png assets/JUC并发编程/image-20240804193339617.png

set、get方法會去檢查所有鍵為null的Entry物件 assets/JUC并发编程/image-20240805161352947.png assets/JUC并发编程/image-20240805161403400.png assets/JUC并发编程/image-20240805161416361.png assets/JUC并发编程/image-20240805161424677.png

阿里巴巴

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>transmittable-thread-local</artifactId>
    <version>2.11.2</version>
</dependency>

阿里巴巴開源的 TransmittableThreadLocal (TTL) 是一個增強版的 ThreadLocal,它可以在建立新執行緒時傳遞父執行緒的上下文。

使得父執行緒設定的值可以在子執行緒中傳遞,解決了執行緒池環境下 ThreadLocal 的資料傳遞問題。

總結

pp; pp assets/JUC并发编程/image-20240805164051846.png

ThreadLocal 出現的意義:

多執行緒 : 讓每個執行緒都有自己獨有的空間,減少鎖的使用 單執行緒: 如儲存目前工作階段或交易資訊,在目前執行緒中傳遞資訊 assets/JUC并发编程/image-20240822190019246.png 單執行緒使用案例 ../../实战项目/assets/苍穹外卖/image-20231029165406992.png

如果設定為static則是每個執行緒存取都是同一個ThreadLocal實例 一般也只new一個ThreadLocal實例,多執行緒使用依舊是每個執行緒自己獨有的變數

單執行緒下使用場景 : 當一個執行緒執行過程中,開始時使用的資料之後又需要使用,就可以用ThreadLocal來進行儲存,以便該執行緒之後的使用

ThreadLocal 和 static 變數在某種程度上可以看作是相反的形式:

總結:

  • ThreadLocal 變數是執行緒私有的,每個執行緒都有自己的副本。

  • static 變數是全域共享的,所有執行緒共享同一個實例。

  • remove 當 ThreadLocal 不再使用的時候, 而 ThreadLocalMap 中依舊存有 ThreadLocal 導致無法回收所以設計為弱引用 ThreadLocal, 讓 ThreadLocal 可以正常被回收了, 但 ThreadlocalMap 中鍵對值卻無法存取了, 導致 value 無法被回收, 所以需要手動 remove, set/get 也會檢查所有鍵為 null 的 Entry 物件

java中多個物件之間通常設計強弱引用關係

Java 記憶體配置和物件標頭

JVM 上記憶體與垃圾回收 > 物件實例化,記憶體配置與存取定位

../JVM/assets/上内存与垃圾回收/image-20240113111239156.jpeg assets/JUC并发编程/image-20240805180824838.png

  1. 物件標頭 (16 位元組)
    1. 物件標記 Mark word (8 位元組)
    2. 類型指標 (8 位元組) assets/JUC并发编程/image-20240805174002599.png Mark word 8 個位元組, 64 位的分布 assets/JUC并发编程/image-20240805174024577.png 類型指標 就是指向方法區類範本檔案的指標, 為了確定是哪個類別的實例 記憶體對齊
  2. 實例資料: 類別的屬性資訊,父類別的屬性資訊,如果是陣列的實例部分包括陣列的長度,按照 4 位元組對齊
  3. 對齊填充: 虛擬機器要求物件起始位址必須是 8 位元組的整數倍。填充資料不是必須存在的,僅僅是為了位元組對齊,這部分記憶體按 8 位元組補充對齊。

Mark word (8 位元組) 為 64bit

assets/JUC并发编程/image-20240805211131947.png

64bit 各種組合就可以看出有無加鎖,加的是什麼鎖

assets/JUC并发编程/image-20240805210928447.png 最後兩位就代表了不同的鎖狀態

assets/JUC并发编程/image-20240806115250551.png 4bit 可以表示 0到15 之間的數值分代年齡用 4bit 表示,所以不能支援改為 16 assets/JUC并发编程/image-20240806115240483.png

java

assets/JUC并发编程/image-20240805215207896.png assets/JUC并发编程/image-20240806115430261.png assets/JUC并发编程/image-20240806115446412.png 類型指標被壓縮了,從8個壓縮到了4個 -XX:+UseCompressedClassPointers


<!--
官網:http://openjdk.java.net/projects/code-tools/jol/
定位:分析物件在JVM的大小和分布
-->
<dependency>
    <groupId>org.openjdk.jol</groupId>
    <artifactId>jol-core</artifactId>
    <version>0.9</version>
</dependency>


ackage com.atguigu.juc.senior.inner.object;
import org.openjdk.jol.vm.VM;
/**
 * @auther zzyy
 * @create 2020-06-13 11:24
 */
public class MyObject
{
    public static void main(String[] args){
        //VM的細節詳細情況
        System.out.println(VM.current().details());
        //所有的物件配置的位元組都是8的整數倍。
        System.out.println(VM.current().objectAlignment());
    }
}

Sychronized 與鎖升級

assets/JUC并发编程/image-20240806145512914.png

效能變化

java 執行緒是對應到作業系統原生執行緒之上,需要在使用者態與核心態之間切換,消耗大量系統資源

核心態與使用者態

使用者態與核心態是描述作業系統中不同權限等級的術語。 使用者態執行於較低權限等級,主要用於一般應用程式,限制對硬體與系統資源的直接存取。 核心態執行於最高權限等級,允許對所有硬體與系統資源進行直接存取與控制,主要用於作業系統核心與關鍵系統服務。

assets/JUC并发编程/image-20240823160622326.png assets/JUC并发编程/image-20240823160602009.png

早期版本中,synchronized屬於重量級鎖,效率低下,因為監視器鎖(monitor)是依賴於底層的作業系統的 Mutex Lock 來實現的 Java 6 之後,為了減少取得鎖與釋放鎖所帶來的效能消耗,引入了輕量級鎖與偏向鎖

簡介

assets/JUC并发编程/image-20240806143944206.png assets/JUC并发编程/image-20240806143954808.png assets/JUC并发编程/image-20240806144455667.png

Mutex Lock assets/JUC并发编程/image-20240806145426252.png

概覽圖

assets/JUC并发编程/image-20240806145621368.png assets/JUC并发编程/image-20240806145658941.png

鎖種類與升級步驟

由物件標頭中 Mark Word 根據鎖標誌位的不同而被複用及鎖升級策略

無鎖

無鎖

無鎖:初始狀態,一個物件被實例化後,如果沒有被任何執行緒競爭鎖,那麼就是無鎖狀態

assets/JUC并发编程/image-20240806115446412.png 0 4 的 01 代表無鎖

物件雖然產生了,但是沒有呼叫 hashcode 是不會記錄 hash 編碼的

從右下角往上看 unused assets/JUC并发编程/image-20240806171505230.png

https://www.bilibili.com/video/BV1ar4y1x727?p=124&vd_source=17535a06cd4587318e9b67ac64afeab7

最後八位 assets/JUC并发编程/image-20240806171606932.png assets/JUC并发编程/image-20240806171613125.png

由物件標頭中

assets/JUC并发编程/image-20240805210928447.png

偏向鎖

預設開啟,但是有延遲4s,延遲時可能跳過偏向鎖加上輕量鎖

實際執行時經常出現一個執行緒很多次搶到一個鎖,那麼在未來的一段時間內,很可能會被同一個執行緒再次鎖定。因此,為了減少取得鎖的開銷,JVM 會在物件標頭中儲存鎖資訊,用於記錄目前持有鎖的執行緒 ID,這樣在未來取得鎖時,可以快速判斷是否是同一個執行緒取得鎖,該執行緒在後續存取時便會自動獲得鎖

JVM 使用 CAS 操作把執行緒指標 ID 記錄到 Mark Word 當中,並修改偏向標示,標示目前執行緒就獲得該鎖。鎖物件變成偏向鎖(透過 CAS 修改物件標頭裡的鎖標誌位),字面意思是偏向於第一個獲得它的執行緒的鎖。執行完同步程式碼區塊後,執行緒並不會主動釋放偏向鎖。

該執行緒無需再進 Monitor 去競爭物件,其他執行緒依舊需要。

執行緒的排程是由作業系統控制的,無法精確控制執行緒的執行順序,因此可能會出現某個執行緒多次搶到鎖的情況。

簡介

assets/JUC并发编程/image-20240806183041173.png assets/JUC并发编程/image-20240806185745623.png assets/JUC并发编程/image-20240806185759909.png assets/JUC并发编程/image-20240806184707495.png assets/JUC并发编程/image-20240806185722224.png assets/JUC并发编程/image-20240806185726690.png

偏向鎖測試

預設開啟,但是有延遲 4 秒,延遲時可能跳過偏向鎖加上輕量鎖

java -XX:+PrintFlagsInitial | grep BiasedLock* assets/JUC并发编程/image-20240806190426488.png

assets/JUC并发编程/image-20240806190415086.png

或等待 5 秒,偏向鎖開啟的時候 assets/JUC并发编程/image-20240806190511629.png

輕量鎖

java15 以後逐步廢棄偏向鎖,開銷與成本大

assets/JUC并发编程/image-20240807205908770.png

本質就是 自旋鎖CAS 先自旋再阻塞,有執行緒來參與鎖的競爭,但是取得鎖的 衝突時間極短

輕量級鎖是為了在執行緒近乎交替執行同步區塊時提高效能

一旦有別的執行緒競爭,發現 Mark Word 中儲存目前執行緒 ID 不是自己的,就會採用 CAS 嘗試取得鎖,成功取得則將 Mark Word 中 ID 替換為自己的,失敗則自旋等待,偏向鎖升級為輕量鎖

assets/JUC并发编程/image-20240807205110422.png assets/JUC并发编程/image-20240807214843898.png 當取得偏向鎖失敗,會升級為輕量鎖, JVM 會為每個執行緒在 目前執行緒的堆疊框架中建立用於儲存鎖記錄的空間 (在 JVM 堆疊中),官方稱為 Displaced Mark Word。若一個執行緒取得鎖時發現是輕量級鎖,會把鎖的 MarkWord 複製到自己的 Displaced Mark Word. 裡面。然後執行緒嘗試用 CAS 將鎖的 MarkWord 替換為 指向鎖記錄的指標。如果成功,當前執行緒取得鎖,如果失敗,表示 Mark Word 已經被替換成了其他執行緒的鎖記錄,說明在與其它執行緒競爭鎖,當前執行緒就嘗試使用自旋來取得鎖。

保證執行緒間公平競爭鎖

偏向鎖的撤銷
  1. 該執行緒執行完
  2. 另一個執行緒進行搶奪

當有另外執行緒逐步來競爭鎖的時候,就不能再使用偏向鎖了,要升級為輕量級鎖

競爭執行緒嘗試CAS更新物件頭失敗,會等待到全局安全點(此時不會執行任何程式碼)撤銷偏向鎖。

assets/JUC并发编程/image-20240807091419806.png

assets/JUC并发编程/image-20240807091651181.png assets/JUC并发编程/image-20240807091703597.png assets/JUC并发编程/image-20240807092515137.png assets/JUC并发编程/image-20240807092532804.png

重量鎖

自旋次數 : 自適應自旋是一種優化技術,它根據當前執行緒在上一次自旋嘗試取得鎖時的表現來動態調整下一次自旋的次數。然後會升級為重量鎖

大部分情況下會使用輕量級鎖而不是重量級鎖

assets/JUC并发编程/image-20240807222404057.png

總結

一句話: 先自旋,不行重量鎖

  • 偏向鎖 偏向鎖: Mark Word 中儲存當前執行緒 ID (延遲 4s)這樣在未來取得鎖時,可以快速判斷是否是同一個執行緒取得鎖, 該執行緒在後續存取時便會自動獲得鎖 偏向鎖取得後並不會主動釋放,如果取得偏鎖失敗就會升級為輕量鎖 為了解決只有在一個執行緒執行同步時提高效能。現代多核處理器經常競爭,所以偏向鎖被廢除

  • 偏向鎖 到 輕量鎖 一旦有別的執行緒競爭,發現 Mark Word 中儲存當前執行緒 ID 不是自己的,就會採用 CAS 嘗試取得鎖,成功則還是偏鎖,失敗則會等待到全局安全點,持有偏向鎖的執行緒會撤銷原來的偏向鎖, 升級為輕量鎖,把鎖的 MarkWord 複製到自己的 Displaced Mark Word. 裡面。然後執行緒嘗試用 CAS 將鎖的 MarkWord 替換為 指向鎖記錄的指標。 偏向鎖 和 輕量鎖 都不涉及內核到使用者態的切換

  • 輕量鎖 輕量鎖 自旋鎖CAS, 有執行緒來參與鎖的競爭,但是取得鎖的 衝突時間極短, 輕量級鎖是為了在執行緒 近乎交替 執行同步區塊時提高效能

  • 到重量鎖 在多執行緒競爭條件下,可能出現偏向鎖跳過輕量鎖直接升級為重量鎖 或輕量鎖直接升級為重量鎖

  1. 多個執行緒頻繁爭搶同一個鎖:當多個執行緒頻繁嘗試取得同一個鎖時,偏向鎖會被撤銷,並直接升級為重量級鎖。
  2. 執行緒數超過閾值:JVM 有一個閾值,當嘗試取得鎖的執行緒數超過這個閾值時,偏向鎖會直接升級為重量級鎖。

JVM會根據執行緒競爭的激烈程度和系統的負載情況動態調整這個閾值,超過閾值就會升級

assets/JUC并发编程/image-20240808001958197.png

鎖升級後雜湊碼去哪了?

偏鎖不能和雜湊碼共存 如果在使用鎖前計算雜湊碼會直接升級為輕量鎖 如果在使用鎖時計算雜湊碼會直接升級為重量鎖

輕量鎖可以共存 assets/JUC并发编程/image-20240807231547249.png

當物件計算過雜湊碼後,就不能進入鎖,否則就會造成編碼錯亂 o.hashCode() assets/JUC并发编程/image-20240807223214025.png assets/JUC并发编程/image-20240807223224017.png assets/JUC并发编程/image-20240807231320446.png assets/JUC并发编程/image-20240807232722348.png

各種鎖的優缺點

assets/JUC并发编程/image-20240807234747635.png

鎖消除:JIT 會忽略沒有共用擴散到其他執行緒的鎖

鎖粗化:一定範圍內都是一個鎖,申請一把鎖即可,編譯器會進行最佳化

JIT 對鎖的最佳化

鎖消除 assets/JUC并发编程/image-20240808000541721.png assets/JUC并发编程/image-20240808000500958.png

鎖粗化 assets/JUC并发编程/image-20240808000531269.png assets/JUC并发编程/image-20240808000520410.png

AbstractQueuedSynchronizer 之 AQS

抽象的佇列同步器,有很多沒有搶到鎖,此時需要一個佇列來對這些還需要搶鎖的執行緒進行管理

是用來建構鎖或者其它同步器元件的重量級基礎框架及整個 JUC 體系的基石, 透過內建的 FFO 佇列來完成資源獲取執行緒的排隊工作,並透過一個int 類別變數表示持有鎖的狀態

AbstractQueuedSynchronizer 簡稱為 AQS

assets/JUC并发编程/image-20240809192711591.png assets/JUC并发编程/image-20240810000149882.png assets/JUC并发编程/image-20240810000219803.png

內部結構

assets/JUC并发编程/image-20240812174131887.png

assets/JUC并发编程/image-20240812205144017.png

assets/JUC并发编程/image-20240812182638121.png 阻塞入隊

assets/JUC并发编程/image-20240812182626469.png

assets/JUC并发编程/image-20240812213413414.png

ReentrantLock

ReentrantLock 就是操作 sync,對外保留 FairSync/NofairSync

lock-->acquire-->tryAcquire-->addWaiter-->acquireQueued

assets/JUC并发编程/image-20240814003145227.png 假設 A 需要時間特別長,在這種情況下進行講解

入隊

lock

assets/JUC并发编程/image-20240812220109122.png assets/JUC并发编程/image-20240812223841754.png

  1. lock 非公平上來直接 CAS 進行搶佔,搶不到進入acquire 公平進入acquire

assets/JUC并发编程/image-20240812223849067.png assets/JUC并发编程/image-20240812224702621.png

acquire
  1. acquire沒有實際方法,參數為 int 類型,代表鎖的可重用性

呼叫 tryAcquire acquire 為父類別的AbstractQueuedSynchronizer的,兩個類別都一樣,會呼叫子類別重寫過的tryacquire

assets/JUC并发编程/image-20240812225316742.png

tryAcquire
  1. tryAcquire(int)進行搶鎖

公平鎖多了個hasQueuedPredecessors(),其他都一樣 getstatue是 1,說明有別的執行緒佔用

非公平鎖中 else if (current == getExclusiveOwnerThread()) 保證鎖的可重用性

assets/JUC并发编程/image-20240812220129562.png assets/JUC并发编程/image-20240812220118289.png

assets/JUC并发编程/image-20240812224725010.png

兩種鎖唯一的區別就在於公平鎖在獲取同步狀態時多了一個限制條件: hasQueuedPredecessors() assets/JUC并发编程/image-20240812220133497.png

addWaiter

assets/JUC并发编程/image-20240812225316742.png

  1. 說明上面的搶鎖失敗,進入等待addWaiter(Node.EXCLUSIVE)建立 Node 並 加入等待佇列

mode有兩種: SHARED共享等待鎖 EXCLUSIVE獨佔等待鎖

tail初始為null

assets/JUC并发编程/image-20240813152543390.png

然後開始直接進入enq,只會執行一次,進行初始化 enq 方法只有第一次添加 node 才會呼叫,第二次則不會呼叫

assets/JUC并发编程/image-20240813232005592.png

t為上一個尾部 node.prev=t 當前 node 的前一個節點指向上一個尾部 compareAndSetTail(pred, node)更新尾部,設定成當前 node t.next將上一個節點指向當前節點 上半部分程式碼的作用是初始化建立一個空的節點,並將頭節點和尾節點都指向這個新建立的節點。

assets/JUC并发编程/image-20240813153927886.png 然後 t 不再為 null,而是第一個虛擬節點,不進入第一個 if,進入 else 這串程式碼將第一個入隊的節點設定為尾節點

assets/JUC并发编程/image-20240813194420792.png

佇列中是如何喚醒、排隊、穩定在佇列中的

acquireQueued
  1. acquireQueued(node, arg) 在等待佇列中阻塞當前執行緒,直到獲取同步狀態成功。

assets/JUC并发编程/image-20240813234052960.png

head 最開始為初始的預設空 node,之後都是當前正在使用鎖的節點 前一個為 head,說明排到了自己然後繼續 tryAcquire,假設現在其他執行緒還在處理,則不進入

failed 變數的存在是為了確保在任何異常或中斷情況下,資源能夠被正確釋放和清理。

assets/JUC并发编程/image-20240814094651109.png

然後進入 shouldParkAfterFailedAcquire 對狀態更改

上一個節點的初始 waitStatus 預設 0,然後進入 compareAndSetWaitStatus(pred, ws, Node.SIGNAL); Node. SIGNAL 為 -1,改為了 -1,返回 flase 就跳出了,進入下一個迴圈,再次進入 shouldParkAfterFailedAcquire 對狀態更改,第二次進入就是 -1 返回了 true,然後 LockSupport.park(this); ,this 為當前 node,進行了阻塞,會等待通行證,這就是 AQS 底層如何做到 node 停在這裡的

然後 C 節點進入,同樣 waitStatus, 第一次 0 返回 false 然後改為 -1 返回ture,同樣設置了 park(this)

assets/JUC并发编程/image-20240814002243719.png

也就是後面的節點都會把前面的 waitStatus 改為 -1, 然後阻塞自己

只有虛擬節點的後一個 node 或當前使用鎖的節點的下一個節點 且搶佔 tryacquire 成功才會進入藍色的

佇列中再往後的節點都會進入紅色的

assets/JUC并发编程/image-20240814100942757.png

assets/JUC并发编程/image-20240814000519279.png assets/JUC并发编程/image-20240814001414610.png assets/JUC并发编程/image-20240814001604251.png

此時 B 就卡在這轉圈

出隊

unlock-->sync.release(1)-->tryRelease(arg)-->unparkSuccessor

sync.release(1)

assets/JUC并发编程/image-20240814004536401.png assets/JUC并发编程/image-20240814004554941.png

tryRelease(arg)

assets/JUC并发编程/image-20240814004828925.png Thread.currentThread() != getExclusiveOwnerThread() 確保當前呼叫解鎖方法的執行緒是持有鎖的執行緒。 如果當前執行緒不是持有鎖的執行緒,則拋出 IllegalMonitorStateException 異常。 防止其他執行緒在沒有持有鎖的情況下嘗試釋放鎖,保證鎖的正確性和一致性。

state 是 1, 減去 1 為 0, 然後就釋放該執行緒

然後 tryRelease(arg) 運行完畢, 返回 ture

assets/JUC并发编程/image-20240814004554941.png h != null 頭節點肯定不為null h.waitStatus != 0 頭節點的 waitStatus,只要其他執行緒來搶了,肯定也不等於 0

unparkSuccessor

然後呼叫 unparkSuccessor(h) 傳入head 重新把 虛擬節點 waitStatus 中之前的 -1 設置為 0

assets/JUC并发编程/image-20240814010556372.png

此時 s 為虛擬節點的 下一個節點

第一個 if,S==null 為 false 不進入 第二個 進入

if (s != null)  
    LockSupport.unpark(s.thread);

釋放了通行證 釋放通行證後, parkAndCheckInterrupt,返回的還是 flase,但在 for (;;) 中,此時 tryAcquire 假設成功,可以順利進入藍色的 (非公平鎖並不是在佇列中先進先出,可能直接被別的執行緒搶到)

assets/JUC并发编程/image-20240814001414610.png assets/JUC并发编程/image-20240814002741932.png

setHead

將當前最新的 node 設定為 head node.thread = null; node.prev = null; assets/JUC并发编程/image-20240814095625089.png 當 node 成功取得鎖後,thread 變數就不再需要了。此時,node 已經成為佇列的頭節點,執行緒資訊不再需要保留在節點中。透過將 thread 欄位設定為 null,可以幫助垃圾回收器回收不再需要的物件,從而避免記憶體洩漏 assets/JUC并发编程/image-20240814095917970.png assets/JUC并发编程/image-20240814100143058.png 取得後就不需要 thread 了 assets/JUC并发编程/image-20240814100314540.png

p.next 設定為 null,讓上一個節點指向的下一個設定為 null,此時可以被垃圾回收

assets/JUC并发编程/image-20240814094608188.png assets/JUC并发编程/image-20240814100609468.png

取消流程

assets/JUC并发编程/image-20240814102201023.png

assets/JUC并发编程/image-20240814164324379.png assets/JUC并发编程/image-20240814164347508.png

這通常用於跳過已取消的節點,確保當前節點的前驅節點是一個有效的、未取消的節點。
把node的prev的prev賦給現在的node.prev,直到找到前節點不取消的那個

while (pred.waitStatus > 0)  
    node.prev = pred = pred.prev;

總結

Node

每個 node 節點包含上一個與下一個資訊,佇列包含 head/tail 資訊,這樣就構成了一個佇列 assets/JUC并发编程/image-20240814095244600.png assets/JUC并发编程/image-20240814094608188.png assets/JUC并发编程/image-20240812204421348.png

簡介

assets/JUC并发编程/image-20240812204208264.png assets/JUC并发编程/image-20240812204227788.png

StampedLock

Java SE 與 3.應用程式開發與多執行緒 > ReentrantLock

ReentrantLock-->ReentranReadWriteLock-->StampedLock

ReentranReadWriteLock 當讀很多寫很少,可能出現寫鎖飢餓,搶不到鎖的情況

引出了 郵戳鎖StampedLock

StampedLock 採取樂觀取得鎖後,其他執行緒嘗試取得寫鎖時不會被阻塞,這其實是對讀鎖的優化,所以,在取得樂觀讀鎖後,還需要對結果進行校驗。 assets/JUC并发编程/image-20240817173505236.png

assets/JUC并发编程/image-20240817174023103.png 郵戳鎖StampedLock 就是讀的過程中允許寫鎖介入,避免了寫鎖飢餓 assets/JUC并发编程/image-20240817174519401.png

static StampedLock stampedLock = new StampedLock(); long stamp = stampedLock.tryOptimisticRead(); stampedLock.validate(stamp) 檢查是否變動過,false 代表有修改 stampedLock.readLock() 和讀寫鎖一樣 stampedLock.writeLock() 和讀寫鎖一樣

note

assets/JUC并发编程/image-20240817182159170.png

  • 缺點

StampedLock 不支援重入,沒有 Re 開頭 StampedLock 的悲觀讀鎖和寫鎖都不支援條件變數 (Condition),這個也需要注意。 使用 StampedLock 一定不要呼叫中斷操作,即不要呼叫 interrupt()方法

思維

  • 多執行緒時,任意兩行程式碼執行之間都可能存在別的執行緒執行了其他程式碼
  • 它是什麼,能幹什麼,怎麼會出現的,沒有它之前是怎麼做的,解決了哪些痛點,方便了哪些地方