JUC 並發:從 Future 到 CompletableFuture
前置複習
../3.应用程序开发/多线程 Java SE/3.應用程式開發/多執行緒 > 實作 Callable 介面 THREAD 常用方法
本質都是 Thread 類別呼叫 start(),start()方法來啟動新執行緒
- 繼承
Thread類別,改寫run方法 - 傳給
Thread類別runnable實作類別,改寫run方法 - 建立
Callable(实现call方法)-->傳給FutureTask-->傳給Thread也可直接call.call():直接於目前執行緒中執行Callable任務(同步執行),這只是普通的方法呼叫,在目前執行緒中同步執行,不是多執行緒(解決傳統Runnable介面無法傳回值與擲出受檢異常的問題。) Executors建立執行緒池
CompletableFuture 並不直接建立執行緒,它是對執行緒池(通常是 ForkJoinPool.commonPool())的封裝, 利用了 多執行緒(或者執行緒池)來實現非同步程式設計。
當使用 CompletableFuture 的非同步方法(例如 supplyAsync, runAsync)時,它會將任務提交給執行緒池執行
Java SE/3.應用程式開發/多執行緒 > 實作 Callable 介面
並發,concurrent) 同一 實體的多個事件,一個 處理器上同時處理多個任務 (科學運算、影像處理中的大規模資料平行處理
)
平行,parallel): 不同 實體的多個事件,多個處理器處理多個任務(Web 伺服器處理多個用戶端請求)

平行後並發
管程 : Monitor 監視器,也就是平時說的 鎖
JVM 靠 管程 來實現方法層級的同步與方法內部一段指令序列的同步
使用者執行緒 : 系統工作執行緒
守護執行緒 : 為其他執行緒服務,如 GC
isDaemon 判斷執行緒是使用者false還是守護 true

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; })
上面的輪詢方式並不優雅,還應該滿足以下需求
- 完成能夠主動告訴
- 能夠將不同的非同步結果組合
- 能夠選擇出執行最快的
CompletionStage 介面
CompletionFuture 是 CompletionStage 介面和 Future 介面的一個具體實作類別,提供了更多的功能和操作方法來處理非同步計算任務。
- 非同步任務
結束/出錯時,會自動回呼某個物件的方法; - 主執行緒設定好回呼後,不再關心非同步任務的執行,非同步任務之間可以順序執行
runAsyncsupplyAsync
不推薦使用 CompatableFuture 的預設空參建立,因為這樣建立的 CompletableFuture 實例並沒有指定具體的非同步任務,可能會導致在後續使用時出現邏輯錯誤或異常。正確的做法是在建立 CompletableFuture 實例時傳入具體的非同步任務,以確保程式的正確性和穩定性

如果使用 runAsync,再 get 則還是之前的阻塞式
runAsync
執行緒池 類似於 守護執行緒 會在 Main 執行緒結束後立刻結束,因此要確保 main 執行緒最後結束
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 函数式接口
轉換採用多執行緒案例,模擬查詢
將list中儲存的List<NetMall>取出轉化成List<CompletableFuture<String>>再轉換成List<String>
用到stream中的中間方法map Java SE/新特性/Stream流#中间方法
將NetMall物件轉換為一個CompletableFuture物件,並回傳一個代表非同步計算結果的CompletableFuture物件
不使用lambda的寫法
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 常用方法
runAsyncsupplyAsyncget()get()
3 對計算結果進行消費
thenRun 不需要前一步的結果,會使用之前的執行緒池
thenRunAsync 非同步,會使用ForkJoinPool,不使用自訂執行緒池
thenAccept需要前一步結果,無傳回值,它傳回的是一個新的 CompletableFuture<Void>
thenApply需要前一步結果,有傳回值
thenRunAsync 會使用ForkJoinPool,不使用自訂執行緒池
join() 方法會阻塞目前執行緒,等待非同步工作執行完成。這樣可以確保在需要取得非同步工作結果的地方,能夠取得最終的結果
- 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 對計算結果進行合併
鎖
synchronized方法 (鎖this)
一個物件裡面如果有多個 synchronized方法,只要一個執行緒去呼叫其中的一個synchronized方法了,該物件其它的執行緒的synchronized方法都只能等待 也就是同一物件,只能有唯一的一個執行緒去存取這些synchronized方法
如果兩個物件,則不是同一把鎖,則各自呼叫各自的 synchronized 方法,不會有影響
static synchronized方法 (鎖類別範本檔案)
會加類別鎖,所有實例都會被鎖定
對於一般同步方法,鎖的是目前實例物件,通常指 this,具體的一支支手機,所有的一般同步方法用的都是同一把鎖>實例物件本身
對於靜態同步方法,鎖的是目前類別的Class物件,如Phone.class唯一的一個範本
synchronized
為什麼每個物件都可以成為一個鎖
ReentrantLock 公平和非公平鎖(預設)
公平:多個執行緒按照申請鎖的順序來取得鎖 非公平鎖:不按照申請鎖順序,可能後申請的執行緒比先申請的執行緒優先取得鎖,在高並發的環境下,有可能造成優先級反轉或鎖飢餓狀態
公平非公平的選擇
非公平:吞吐量,節省時間 公平:雨露均霑
可重入鎖(遞迴鎖)
JAVA 微服務生態/Redis7/基礎/Redis 高級篇 > 可重用鎖
死鎖案例以及如何排除死鎖
互斥條件/不可搶奪/佔用且等待/循環等待
小節
LockSupport 與執行緒中斷
執行緒中斷機制
interrupt/interrupted/isinterrupted
是協商機制,不是強制性,執行緒自行決定是否中斷
interrupt 會設定中斷狀態為 True,並不能中斷執行緒,只是充當一個 flag,需要自己手動程式碼配合
如果執行緒本身處於 sleep,wait,join 狀態中,別的執行緒呼叫 interrupt 設定為 true,會拋出例外 interruptedException,例外會把 interrupt 狀態清除,變為 false,因此需要在例外處理的地方再次呼叫來確保執行緒終止
interrupted:回傳current執行緒的中斷狀態,然後重新把目前的狀態設定為false
也就是假如中斷狀態為 True,呼叫 interrupted 回傳 current 執行緒的中斷狀態true,然後設定為false
exception interruptedException 發生後會把 interrupt 設定為 false,會導致其他執行緒設定的 true 失效
在沒有interrupt時,使用中斷標識中斷
底層都是透過 isInterrupted 實作
LockSupport
LockSupport是用來建立鎖和其他同步類別的基本執行緒阻塞原語
執行緒等待喚醒機制
Object 類別的wait/notify 必須在同步程式碼區塊或同步方法中成對出現
wait/notify
Java SE/3.應用程式開發/多執行緒 > new Condition
在這個範例中,awaitCondition 方法會使目前執行緒等待,直到 signalCondition 方法發出訊號
Object和Condition 的限制是
- 必須先取得鎖或
lock - 必須先等待後再喚醒
LockSupport 解決了上面的限制
無需鎖區塊
支援先喚醒後等待(憑證設計)
先執行 unpark 會設定一個許可證,即使 park 在 unpark 後執行,已經有了許可證,直接進行放行,會消耗一個許可證
unpark 會增加一個許可證,不可以累計,最多只有一個,呼叫多次 unpark 也是只有一個許可證
Java 記憶體模型 JMM
- JMM 規範:JMM 規範控制著執行緒工作記憶體與主記憶體之間的資料同步。
- 何時將主記憶體中的資料同步到執行緒工作記憶體:
- 何時將執行緒工作記憶體中的資料刷新到主記憶體
- 提供 happens-before 關係: JMM 定義了一套 happens-before 規則,這些規則強制規定了某些操作之間的 happens-before 關係,
JVM 屏蔽掉各種硬體作業系統記憶體存取差異, JVM 本身是抽象的概念,描述一組規範,規範定義了程式中(尤其是多執行緒)各個變數的讀寫存取方式,並決定一個執行緒對共享變數的寫入何時以及如何變成對另一個執行緒可見,關鍵技術點都是圍繞多執行緒的 原子性、可見性和有序性展開的。
JVM 三大特性(可見、原子、有序)
可見性
- 原子性:一個操作不可以中斷,多執行緒下不能被其他執行緒干擾
- 有序性 程式碼由上到下,有序執行
Happens-before
就是將複雜的底層記憶體操作和處理器行為抽象成一組我們可以理解的規則(也就是底層是如何執行我們程式碼的),這個規則就是 HB 規則,只需要遵循這些 HB 規則就可以保證並行程式的正確性,而無需關心底層是如何實現的
如果你程式碼的執行不在 HB 規則中,就無法保證該程式碼執行時是否安全
volatile 與 Java 記憶體模型
寫 volatile 時,JMM 將該執行緒對應本地記憶體的共享變數值立刻刷新到主記憶體
讀 volatile 時,JMM 將 該執行緒對應本地記憶體的共享變數值 設定為無效,直接從主記憶體讀取共享變數
也就是 volatile 讀寫都是直接和主記憶體互動
記憶體屏障
volatile 底層實現透過記憶體屏障
記憶體屏障是一種屏障指令,它使得 CPU 或編譯器對屏障指令的前後所發出的記憶體操作,執行一個排序的約束,也叫記憶體柵欄或柵欄指令
記憶體屏障保證了 volatile 的可見性,只保證 volatile 變數
四大記憶體屏障插入策略分類
規律,XXYY XX先執行後再執行YY
StoreLoad防止後續的 volatile 讀操作重排,但也間接的防止 volatile 寫操作(因為 volatile 寫前本身會再加StoreStore),可能間接地防止普通寫重排,也可能不防止
第一個操作和第二個操作代表程式碼中的先後順序
如果不禁止重排,那麼 volatile 讀操作之後的普通讀寫操作可能會被重排到 volatile 讀操作之前,從而破壞了 happens-before 關係,導致程式行為不可預測。
普通讀寫操作很可能依賴於 volatile 讀操作的結果
當第二個操作為volatile寫時,不論第一個操作是什麼,都不能重排序。這個操作保證了volatile寫之前的操作不會被重排到volatile寫之後
雖然通常說 StoreLoad 有防止後續的 volatile 讀或寫操作重排序到 volatile 寫操作之前的作用,但實際上它也間接起到了防止在前的普通讀操作被重排序到 volatile 寫之後的作用。
就算volatile寫前沒有加loadstore,JVM仍然可以透過以下方式保證程式正確

讀之後的操作不會重排到讀之前 寫之前的操作不會重排到寫之後
volatile寫:在每個volatile寫操作前或後插入一個StoreStotre/Storeload屏障
volatile Read在每個volatile Read操作後插入一個LoadLoad,LoadStore屏障
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):關注的是 單一變數 的讀寫操作之間的順序,以及與其他volatile變數讀寫操作的順序。
指令禁重排
volatile
禁止 寫入之前 的操作重排到 volatile 之後(這意味著在 volatile 寫入操作之前的所有寫入操作都已經 完成 並對其他執行緒可見),但寫入之後的操作可能會被重排,此時如果有影響則需要上鎖
也就是說 volatile 讀取 操作 之後 的操作不會重排到讀取之前,寫入 操作 之前 的操作一定已經完成,而 鎖 可以直接避免所有指令重排
volatile 讀取 之後 加 LoadStore 和 LoadLoad
volatile 寫入 之前 加 StoreStore,後加 Storeload
讀取之後的操作不會重排到讀取之前
寫入之前的操作不會重排到寫入之後
寫入之前的操作重排到 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中,我們有三步操作:
x = 1;(普通寫入操作)v = y;(volatile寫入操作)y = 1;(普通寫入操作)
重排序規則
根據Java記憶體模型,volatile寫入操作會在其前後插入記憶體屏障:
- 寫屏障(Store Barrier):在寫入
volatile變數之前,確保所有之前的普通寫入操作都已經完成並對其他執行緒可見。 - 讀屏障(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之前。
因此,可能的執行順序包括:
x = 1y = 1v = 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之後。
使用方式
讀 : volitile 保證可見性 寫 : 鎖保證原子性
多執行緒使用 volatile 禁用reordering避免問題
物件建立分為三步,多執行緒可能發出重排導致出錯,因此可以使用 volatile 禁用 reordering 避免問題
DCL 雙端鎖
雙檢
第二次加鎖因為:可能別的執行緒在開始檢查時沒有建立完成,而檢查鎖時正好建立完成釋放了鎖
由於SafeDoubleCheckSingleton沒有加volatile
singleton = new SafeDoubleCheckSingleton();
這行程式碼實際上分為三個步驟:
- 為
SafeDoubleCheckSingleton分配記憶體空間。 - 呼叫
SafeDoubleCheckSingleton的建構函式來初始化物件。 - 將
singleton參照指向分配的記憶體位址。
由於重排序,可能進行到第二步,物件還沒有建立完畢,另一個執行緒進行判斷以為物件已經建立完成,導致出錯,因此需要volatile保證不會發生重排
總結
原子性
CAS 比較並交換
CAS 的全稱為 Compare-And-Swap,它是一條CPU並發原語。原語的執行必須是連續的,在執行過程中不允許被中斷,也就是說 CAS 是一條 CPU 的原子指令,不會造成所謂的資料不一致問題
它的功能是判斷記憶體某個位置的值是否為預期值,如果是則更改為新的值,這個過程是原子的。採用樂觀鎖重試機制
AtomicInteger 類主要利用CAS (compare and swap) + volatile和 native 方法來保證原子操作,從而避免 synchronized的高開銷,執行效率大為提升
沒有CAS時 : 多執行緒需要加 鎖 來保證原子性
有 CAS 時 : 多執行緒使用原子類可以保證原子性
但是存在ABA 問題
CAS 是什麼
原始碼分析
CAS 是一條 CPU 的原子指令,CPU 並發原句,不會造成所謂的資料不一致問題
Unsafe 是 CAS 的核心類,可以直接呼叫作業系統底層
自旋
嘗試獲取鎖的執行緒不會立即阻塞,而是採用迴圈的方式去嘗試獲取鎖,
當執行緒發現鎖被佔用時,會不斷迴圈判斷鎖的狀態,直到獲取。這樣的好處是減少執行緒上下文切換的消耗,缺點是迴圈會消耗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 問題
ABA 問題
原子操作類之 18 羅漢增強
當執行緒數量增多時,使用原子操作類的優勢會更加明顯
基本型別原子類
AtomicInteger/AtomicBoolean/AtomicLong
class MyNumber
{
@Getter
private AtomicInteger atomicInteger = new AtomicInteger();//The default is 0
public void addPlusPlus()
{
atomicInteger.incrementAndGet();
}
}
Java SE/3.應用程式開發/多執行緒 > CountDownLatch
陣列型別原子類
AtomiclntegerArray
AtomicLongArray
AtomicReferenceArray
參照型別原子類
AtomicReference
AtomicStampedReference 版本號
AtomicMarkableReference
物件的屬性修改原子類
可以以 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();
}
}
}
原子操作增強類原理深度解析
DoubleAccumulator 建立一個初始總和 0 的新加法器,之後和每次傳入的新數做運輸,取決於重寫 applyAsLong 的邏輯
DoubleAdder
LongAccumulator
LongAdder
DoubleAdder,DoubleAccumulator 和效能對比
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 為何快
https://www.bilibili.com/video/BV1ar4y1x727/?p=90&vd_source=17535a06cd4587318e9b67ac64afeab7
92 到 99 沒看
LongAdder原始碼
鎖-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
}
CASCompare-And-Swap
各種原子類,採用樂觀鎖機制
透過硬體級別的指令(如 cmpxchg 指令)來確保比較-更新操作的原子性
通常配合 volatile 實現 可見性,和 部分有序性
沒有 CAS 時 : 多執行緒需要加 鎖 來保證原子性
有 CAS 時 : 多執行緒使用 原子類 可以保證原子性
- 鎖
偏向鎖-輕量-重量
鎖可以直接保證 可見,原子,有序 ,無需volatile
通常情況下,如果並發讀寫操作較多且競爭不激烈,可以考慮使用 CAS 搭配 volatile;如果需要更強的執行緒安全性或者競爭比較激烈,可以選擇直接使用鎖。
ThreadLocal 提供了一種執行緒 區域變數,每個執行緒都有一份,再也不需要使用 Voliatile/CAS/鎖
ThreadLocal
ThreadLocal 提供執行緒區域變數。這些變數與正常的變數不同,因為每一個執行緒在存取 ThreadLocal 實例的時候(透過其 get 或 set 方法)都有自己的、獨立初始化的變數副本。ThreadLocal 實例通常是類別中的私有靜態欄位,使用它的目的是希望將狀態(例如,使用者 ID 或交易 ID)與執行緒關聯起來。
實現每一個執行緒都有自己專屬的本地變數副本 (自己用自己的變數不麻煩別人,不和其他人共享,人人有份,人各一份)
提供了一種執行緒 區域變數,每個執行緒都有一份,再也不需要使用 Voliatile/CAS/鎖
使用方式
- 初始化方式
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 修飾
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 裡
記憶體洩漏問題
ThreadLocalMap 是經過兩層包裝的 ThreadLocal 物件
(1)第一層包裝是使用 WeakReference<ThreadLocal<?>>將ThreadLocal物件變成一個弱引用的物件(藍框)
(2)第二層包裝是定義了一個專門的類別 Entry 來擴充WeakReference<ThreadLocal<?>>
JVM 上記憶體與垃圾回收 > 再談引用
一個執行緒可以使用多個 ThreadLocal 變數,所以 key 使用 ThreadLocal,用於區分同一個執行緒的 ThreadLocalMap 中儲存的不同執行緒區域變數
引用複習
執行緒執行完後 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
為什麼原始碼用弱引用
阿里巴巴
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>transmittable-thread-local</artifactId>
<version>2.11.2</version>
</dependency>
阿里巴巴開源的 TransmittableThreadLocal (TTL) 是一個增強版的 ThreadLocal,它可以在建立新執行緒時傳遞父執行緒的上下文。
使得父執行緒設定的值可以在子執行緒中傳遞,解決了執行緒池環境下 ThreadLocal 的資料傳遞問題。
總結
ThreadLocal 出現的意義:
多執行緒 : 讓每個執行緒都有自己獨有的空間,減少鎖的使用
單執行緒: 如儲存目前工作階段或交易資訊,在目前執行緒中傳遞資訊
單執行緒使用案例

如果設定為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 上記憶體與垃圾回收 > 物件實例化,記憶體配置與存取定位
- 物件標頭 (16 位元組)
- 實例資料: 類別的屬性資訊,父類別的屬性資訊,如果是陣列的實例部分包括陣列的長度,按照 4 位元組對齊
- 對齊填充: 虛擬機器要求物件起始位址必須是 8 位元組的整數倍。填充資料不是必須存在的,僅僅是為了位元組對齊,這部分記憶體按 8 位元組補充對齊。
Mark word (8 位元組) 為 64bit
64bit 各種組合就可以看出有無加鎖,加的是什麼鎖
4bit 可以表示 0到15 之間的數值分代年齡用 4bit 表示,所以不能支援改為 16

java
類型指標被壓縮了,從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 與鎖升級
效能變化
java 執行緒是對應到作業系統原生執行緒之上,需要在使用者態與核心態之間切換,消耗大量系統資源
核心態與使用者態
早期版本中,synchronized屬於重量級鎖,效率低下,因為監視器鎖(monitor)是依賴於底層的作業系統的 Mutex Lock 來實現的
Java 6 之後,為了減少取得鎖與釋放鎖所帶來的效能消耗,引入了輕量級鎖與偏向鎖
鎖種類與升級步驟
由物件標頭中 Mark Word 根據鎖標誌位的不同而被複用及鎖升級策略
無鎖
無鎖
無鎖:初始狀態,一個物件被實例化後,如果沒有被任何執行緒競爭鎖,那麼就是無鎖狀態
物件雖然產生了,但是沒有呼叫 hashcode 是不會記錄 hash 編碼的
https://www.bilibili.com/video/BV1ar4y1x727?p=124&vd_source=17535a06cd4587318e9b67ac64afeab7
由物件標頭中
偏向鎖
預設開啟,但是有延遲4s,延遲時可能跳過偏向鎖加上輕量鎖
實際執行時經常出現一個執行緒很多次搶到一個鎖,那麼在未來的一段時間內,很可能會被同一個執行緒再次鎖定。因此,為了減少取得鎖的開銷,JVM 會在物件標頭中儲存鎖資訊,用於記錄目前持有鎖的執行緒 ID,這樣在未來取得鎖時,可以快速判斷是否是同一個執行緒取得鎖,該執行緒在後續存取時便會自動獲得鎖
JVM 使用 CAS 操作把執行緒指標 ID 記錄到 Mark Word 當中,並修改偏向標示,標示目前執行緒就獲得該鎖。鎖物件變成偏向鎖(透過 CAS 修改物件標頭裡的鎖標誌位),字面意思是偏向於第一個獲得它的執行緒的鎖。執行完同步程式碼區塊後,執行緒並不會主動釋放偏向鎖。
該執行緒無需再進 Monitor 去競爭物件,其他執行緒依舊需要。
執行緒的排程是由作業系統控制的,無法精確控制執行緒的執行順序,因此可能會出現某個執行緒多次搶到鎖的情況。
輕量鎖
java15 以後逐步廢棄偏向鎖,開銷與成本大
本質就是 自旋鎖CAS 先自旋再阻塞,有執行緒來參與鎖的競爭,但是取得鎖的 衝突時間極短
輕量級鎖是為了在執行緒近乎交替執行同步區塊時提高效能
一旦有別的執行緒競爭,發現 Mark Word 中儲存目前執行緒 ID 不是自己的,就會採用 CAS 嘗試取得鎖,成功取得則將 Mark Word 中 ID 替換為自己的,失敗則自旋等待,偏向鎖升級為輕量鎖
當取得偏向鎖失敗,會升級為輕量鎖, JVM 會為每個執行緒在 目前執行緒的堆疊框架中建立用於儲存鎖記錄的空間 (在 JVM 堆疊中),官方稱為 Displaced Mark Word。若一個執行緒取得鎖時發現是輕量級鎖,會把鎖的 MarkWord 複製到自己的 Displaced Mark Word. 裡面。然後執行緒嘗試用 CAS 將鎖的 MarkWord 替換為 指向鎖記錄的指標。如果成功,當前執行緒取得鎖,如果失敗,表示 Mark Word 已經被替換成了其他執行緒的鎖記錄,說明在與其它執行緒競爭鎖,當前執行緒就嘗試使用自旋來取得鎖。
保證執行緒間公平競爭鎖
偏向鎖的撤銷
重量鎖
自旋次數 : 自適應自旋是一種優化技術,它根據當前執行緒在上一次自旋嘗試取得鎖時的表現來動態調整下一次自旋的次數。然後會升級為重量鎖
大部分情況下會使用輕量級鎖而不是重量級鎖
總結
一句話: 先自旋,不行重量鎖
偏向鎖偏向鎖:Mark Word中儲存當前執行緒ID(延遲4s)這樣在未來取得鎖時,可以快速判斷是否是同一個執行緒取得鎖, 該執行緒在後續存取時便會自動獲得鎖 偏向鎖取得後並不會主動釋放,如果取得偏鎖失敗就會升級為輕量鎖 為了解決只有在一個執行緒執行同步時提高效能。現代多核處理器經常競爭,所以偏向鎖被廢除偏向鎖到輕量鎖一旦有別的執行緒競爭,發現Mark Word中儲存當前執行緒 ID 不是自己的,就會採用 CAS 嘗試取得鎖,成功則還是偏鎖,失敗則會等待到全局安全點,持有偏向鎖的執行緒會撤銷原來的偏向鎖, 升級為輕量鎖,把鎖的MarkWord複製到自己的Displaced Mark Word. 裡面。然後執行緒嘗試用CAS將鎖的MarkWord替換為指向鎖記錄的指標。偏向鎖和輕量鎖都不涉及內核到使用者態的切換輕量鎖輕量鎖自旋鎖CAS, 有執行緒來參與鎖的競爭,但是取得鎖的衝突時間極短, 輕量級鎖是為了在執行緒近乎交替執行同步區塊時提高效能到重量鎖 在多執行緒競爭條件下,可能出現偏向鎖跳過輕量鎖直接升級為重量鎖 或輕量鎖直接升級為重量鎖
- 多個執行緒頻繁爭搶同一個鎖:當多個執行緒頻繁嘗試取得同一個鎖時,偏向鎖會被撤銷,並直接升級為重量級鎖。
- 執行緒數超過閾值:JVM 有一個閾值,當嘗試取得鎖的執行緒數超過這個閾值時,偏向鎖會直接升級為重量級鎖。
JVM會根據執行緒競爭的激烈程度和系統的負載情況動態調整這個閾值,超過閾值就會升級
鎖升級後雜湊碼去哪了?
鎖消除:JIT 會忽略沒有共用擴散到其他執行緒的鎖
鎖粗化:一定範圍內都是一個鎖,申請一把鎖即可,編譯器會進行最佳化
AbstractQueuedSynchronizer 之 AQS
抽象的佇列同步器,有很多沒有搶到鎖,此時需要一個佇列來對這些還需要搶鎖的執行緒進行管理
是用來建構鎖或者其它同步器元件的重量級基礎框架及整個 JUC 體系的基石,
透過內建的 FFO 佇列來完成資源獲取執行緒的排隊工作,並透過一個int 類別變數表示持有鎖的狀態
AbstractQueuedSynchronizer 簡稱為 AQS
內部結構
ReentrantLock
ReentrantLock 就是操作 sync,對外保留 FairSync/NofairSync
lock-->acquire-->tryAcquire-->addWaiter-->acquireQueued
入隊
lock
lock非公平上來直接 CAS 進行搶佔,搶不到進入acquire公平進入acquire
acquire
acquire沒有實際方法,參數為 int 類型,代表鎖的可重用性
呼叫 tryAcquire
acquire 為父類別的AbstractQueuedSynchronizer的,兩個類別都一樣,會呼叫子類別重寫過的tryacquire
tryAcquire
tryAcquire(int)進行搶鎖
公平鎖多了個hasQueuedPredecessors(),其他都一樣
getstatue是 1,說明有別的執行緒佔用
非公平鎖中
else if (current == getExclusiveOwnerThread())
保證鎖的可重用性
兩種鎖唯一的區別就在於公平鎖在獲取同步狀態時多了一個限制條件:
hasQueuedPredecessors()

addWaiter
- 說明上面的搶鎖失敗,進入等待
addWaiter(Node.EXCLUSIVE)建立 Node 並 加入等待佇列
mode有兩種:
SHARED共享等待鎖
EXCLUSIVE獨佔等待鎖
tail初始為null
然後開始直接進入enq,只會執行一次,進行初始化
enq 方法只有第一次添加 node 才會呼叫,第二次則不會呼叫
t為上一個尾部
node.prev=t 當前 node 的前一個節點指向上一個尾部
compareAndSetTail(pred, node)更新尾部,設定成當前 node
t.next將上一個節點指向當前節點
上半部分程式碼的作用是初始化建立一個空的節點,並將頭節點和尾節點都指向這個新建立的節點。
然後 t 不再為 null,而是第一個虛擬節點,不進入第一個 if,進入 else
這串程式碼將第一個入隊的節點設定為尾節點
佇列中是如何喚醒、排隊、穩定在佇列中的
acquireQueued
acquireQueued(node, arg)在等待佇列中阻塞當前執行緒,直到獲取同步狀態成功。
head 最開始為初始的預設空 node,之後都是當前正在使用鎖的節點
前一個為 head,說明排到了自己然後繼續 tryAcquire,假設現在其他執行緒還在處理,則不進入
failed 變數的存在是為了確保在任何異常或中斷情況下,資源能夠被正確釋放和清理。
然後進入 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)
也就是後面的節點都會把前面的 waitStatus 改為 -1, 然後阻塞自己
只有虛擬節點的後一個 node 或當前使用鎖的節點的下一個節點
且搶佔 tryacquire 成功才會進入藍色的
佇列中再往後的節點都會進入紅色的
此時 B 就卡在這轉圈
出隊
unlock-->sync.release(1)-->tryRelease(arg)-->unparkSuccessor
sync.release(1)
tryRelease(arg)
Thread.currentThread() != getExclusiveOwnerThread()
確保當前呼叫解鎖方法的執行緒是持有鎖的執行緒。
如果當前執行緒不是持有鎖的執行緒,則拋出 IllegalMonitorStateException 異常。
防止其他執行緒在沒有持有鎖的情況下嘗試釋放鎖,保證鎖的正確性和一致性。
state 是 1, 減去 1 為 0, 然後就釋放該執行緒
然後 tryRelease(arg) 運行完畢, 返回 ture
h != null 頭節點肯定不為null
h.waitStatus != 0 頭節點的 waitStatus,只要其他執行緒來搶了,肯定也不等於 0
unparkSuccessor
然後呼叫 unparkSuccessor(h) 傳入head
重新把 虛擬節點 waitStatus 中之前的 -1 設置為 0
此時 s 為虛擬節點的 下一個節點
第一個 if,S==null 為 false 不進入
第二個 進入
if (s != null)
LockSupport.unpark(s.thread);
釋放了通行證
釋放通行證後, parkAndCheckInterrupt,返回的還是 flase,但在 for (;;) 中,此時 tryAcquire 假設成功,可以順利進入藍色的
(非公平鎖並不是在佇列中先進先出,可能直接被別的執行緒搶到)
setHead
p.next 設定為 null,讓上一個節點指向的下一個設定為 null,此時可以被垃圾回收
取消流程
這通常用於跳過已取消的節點,確保當前節點的前驅節點是一個有效的、未取消的節點。
把node的prev的prev賦給現在的node.prev,直到找到前節點不取消的那個
while (pred.waitStatus > 0)
node.prev = pred = pred.prev;
總結
Node
每個 node 節點包含上一個與下一個資訊,佇列包含 head/tail 資訊,這樣就構成了一個佇列

StampedLock
Java SE 與 3.應用程式開發與多執行緒 > ReentrantLock
ReentrantLock-->ReentranReadWriteLock-->StampedLock
ReentranReadWriteLock 當讀很多寫很少,可能出現寫鎖飢餓,搶不到鎖的情況
引出了 郵戳鎖StampedLock
StampedLock 採取樂觀取得鎖後,其他執行緒嘗試取得寫鎖時不會被阻塞,這其實是對讀鎖的優化,所以,在取得樂觀讀鎖後,還需要對結果進行校驗。

郵戳鎖StampedLock 就是讀的過程中允許寫鎖介入,避免了寫鎖飢餓

static StampedLock stampedLock = new StampedLock();
long stamp = stampedLock.tryOptimisticRead();
stampedLock.validate(stamp) 檢查是否變動過,false 代表有修改
stampedLock.readLock() 和讀寫鎖一樣
stampedLock.writeLock() 和讀寫鎖一樣
- 缺點
StampedLock 不支援重入,沒有 Re 開頭
StampedLock 的悲觀讀鎖和寫鎖都不支援條件變數 (Condition),這個也需要注意。
使用 StampedLock 一定不要呼叫中斷操作,即不要呼叫 interrupt()方法
思維
- 多執行緒時,任意兩行程式碼執行之間都可能存在別的執行緒執行了其他程式碼
- 它是什麼,能幹什麼,怎麼會出現的,沒有它之前是怎麼做的,解決了哪些痛點,方便了哪些地方










































































































































































































































