拋異常時會釋放鎖。
當線程在?synchronized
?塊內部拋出異常時,會自動釋放對象鎖。
public class ExceptionUnlockDemo {private static final Object lock = new Object();public static void main(String[] args) {Thread t1 = new Thread(() -> {synchronized (lock) {System.out.println("線程1獲取到鎖");try {// 模擬操作Thread.sleep(1000);// 拋出異常throw new RuntimeException("模擬異常");} catch (InterruptedException | RuntimeException e) {System.out.println("線程1捕獲異常: " + e.getMessage());}// 異常發生后,鎖已經被釋放System.out.println("線程1執行完畢");}});Thread t2 = new Thread(() -> {// 短暫等待,確保t1先執行try {Thread.sleep(500);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("線程2嘗試獲取鎖");synchronized (lock) {System.out.println("線程2獲取到鎖");}});t1.start();t2.start();}
}
結果:
線程1獲取到鎖
線程1捕獲異常: 模擬異常
線程2嘗試獲取鎖
線程2獲取到鎖
線程1執行完畢
從輸出能夠看出,線程 1 在拋出異常之后,線程 2 就能夠獲取到鎖,這表明線程 1 的鎖已經因為異常而被釋放了。
加鎖的區域就是同步代碼塊
在 Java 中,被鎖保護的代碼區域被稱為同步代碼塊(Synchronized Block)。當多個線程訪問同一把鎖的同步代碼塊時,會強制串行執行,從而保證線程安全。同步的意思就是說比如說一個線程在執行時訪問了一個別鎖的共享資源,在時間片結束后還占著資源,另一個線程也不能訪問
public class SynchronizedExample {private final Object lock = new Object(); // 鎖對象// 方法級同步(隱式鎖為 this)public synchronized void method1() {// 同步方法的代碼}// 代碼塊級同步(顯式指定鎖對象)public void method2() {synchronized (lock) {// 同步代碼塊,同一時刻只能被一個線程執行}}
}
monitor進monitor出
對于我們人來說,一個程序的執行開始和執行結束是可以理解的,但機器不行。我們計算機內部的指令經過編譯之后是一長串的代碼,程序以數組的形式存到頁里,我們加的鎖的區域在數組里不允許同時被訪問。那么我們需要一些表記指出哪些區域的代碼是被鎖的不能同時訪問。
首先我們要進行范圍標記枷鎖加鎖我們知道這塊資源是加鎖標記還得。注意我們加鎖是這樣的。
第一,我們對要操作的資源加上所標記。
第二,我們要操作的資源這塊是有這些指令對資源進行操作,這一堆指令要對資源進行操作,其中指令到這些區域。這些區域對它的操作不能是同時進行的,所以我們加鎖的話有兩個區域,一個是對資源本身加標記,另外一個對操作的資源的指令。也得指定所在的范圍同步范圍。
?monitor進monitor出就是標記這些指令里那一塊是不能同時被線程所操作。
JAVA對象頭
snchronized用的鎖是存在Java對象頭里的。如果對象是數組類型,則虛擬機用3個字寬(Word)存儲對象頭,如果對象是非數組類型,則用2字寬存儲對象頭。在32位虛擬機中,1字寬等于4字節,即32bit.
我們在堆區域創建對象,每一個對象都有鎖表記和對象表記,所屬類型:類的地址指向方法區。
如果有數組的話有數組長度。
如果一個64位的操作系統可以安裝32位或64位的程序。32位的操作系統只可以安裝32位程序。如果cpu的一個時鐘周期能夠運算32位,那他只能安裝32位的操作系統。那如果一個程序是64位的程序,它里邊一個字寬就是64位。是32位的程序,那么它就只能說一個字寬是32位。從這個推理下,我們在64位的操作系統下安裝了32位的程序,那么它一個字就是32位。也就是說字寬最終是受程序的位數影響
?
鎖的升級與對比
在 Java 中,鎖的升級是 JVM 為了優化鎖性能而引入的機制。隨著多線程競爭的加劇,鎖會從無鎖狀態逐步升級為偏向鎖、輕量級鎖,最終升級為重量級鎖。這種升級過程是不可逆的,旨在平衡不同競爭程度下的性能開銷。
1.?鎖升級的流程
鎖的狀態會根據競爭情況逐步升級:
無鎖 → 偏向鎖 → 輕量級鎖 → 重量級鎖
1.1?無鎖狀態(Normal)
- 特點:對象頭的 Mark Word 存儲哈希碼、分代年齡等信息。
- 場景:沒有線程訪問同步塊時。
1.2?偏向鎖(Biased Locking)
- 適用場景:只有一個線程頻繁訪問同步塊。
- 原理:
- 首次獲取鎖時,JVM 會在 Mark Word 中記錄線程 ID(通過 CAS 操作)。
- 該線程后續進入同步塊時,無需任何同步操作,直接判斷 Mark Word 中的線程 ID 是否匹配。
- 優點:無競爭時幾乎無額外開銷,性能最優。
- 缺點:當其他線程嘗試競爭鎖時,需要撤銷偏向鎖,消耗 CPU。
?
?
1.3?輕量級鎖(Lightweight Locking)
- 適用場景:多個線程交替訪問同步塊(無競爭)。
- 原理:
- 線程在棧幀中創建鎖記錄(Lock Record),并通過 CAS 將 Mark Word 復制到鎖記錄中。
- 同時,Mark Word 會指向鎖記錄的地址。
- 如果 CAS 成功,線程獲得輕量級鎖;如果失敗,表示有競爭,鎖會升級為重量級鎖。
- 優點:競爭不激烈時避免線程阻塞,減少用戶態和內核態的切換。不進阻塞隊列,快速循環看看自己請求的資源有沒有解鎖。
- 缺點:頻繁 CAS 操作可能消耗 CPU。
1.4?重量級鎖(Heavyweight Locking)
- 適用場景:多個線程同時競爭鎖。
- 原理:
- 依賴操作系統的互斥量(Mutex),線程會被阻塞并進入等待隊列。
- 鎖釋放時,需要喚醒等待的線程,涉及用戶態和內核態的切換。
- 缺點:線程阻塞和喚醒的開銷大,性能最差。重量解鎖的加鎖和解鎖都挺麻煩,不僅僅是修改對象頭標記這么簡單,還有很多枷鎖過程,枷鎖競爭過程。
?四種鎖的對比
特性 | 無鎖 | 偏向鎖 | 輕量級鎖 | 重量級鎖 |
---|---|---|---|---|
適用場景 | 無同步需求 | 單線程訪問 | 多線程交替訪問 | 多線程同時競爭 |
實現方式 | Mark Word 存儲哈希碼等信息 | Mark Word 存儲線程 ID | Mark Word 指向線程棧中的鎖記錄 | Mark Word 指向 Monitor 對象 |
線程狀態 | 無需阻塞 | 無需阻塞 | 自旋等待 | 阻塞等待 |
性能開銷 | 最低 | 低(首次 CAS) | 中等(CAS 操作) | 最高(內核態切換) |
優缺點 | 無同步開銷 | 無競爭時最優 | 避免線程切換 | 線程頻繁阻塞 / 喚醒 |
釋放機制 | 無需釋放 | 線程退出同步塊時不釋放,其他線程競爭時撤銷 | 解鎖時通過 CAS 恢復 Mark Word | 解鎖時喚醒等待線程 |
現在是幾個線程對資源進行競爭只有一個才能競爭成功!只有一個競爭成功加上了鎖。其他就會競爭失敗。競爭失敗的話會競爭失敗會進阻塞隊列。為什么進入阻塞隊列:成功的線程即使時間片到期了,下次還是選中他可以立馬執行。效率最高。每個加鎖資源對應一個阻塞隊列
?
?然后第一個成功占資源的線程已經執行結束后,會給所有在阻塞隊列的線程發通知,讓他們回到就緒隊列。
比較并且交換
比較并交換(Compare-and-Swap, CAS)?是一種實現無鎖(Lock-Free)算法的原子操作,用于在多線程環境中實現同步。它允許線程在不使用鎖的情況下安全地修改共享數據,從而避免了鎖帶來的上下文切換和線程阻塞開銷。這種方式也是實現了寫后讀。
- 如果內存地址 V 中的值等于預期舊值 A,則將該位置的值更新為新值 B,并返回?
true
。 - 如果內存地址 V 中的值不等于預期舊值 A,則不執行更新,返回?
false
。
但也有缺點,他是分兩步的先比較再交換。比如說線程1先比較,發現沒問題可以交換,但此時時間片到期。線程2進入也比較發現也沒問題,也要交換就有問題了。?他不是原子操作,所以無法實現。只能調用操作系統底層。
流水線技術。他減少了電路的切換。提升的整體速度。但是指令順序會發生變化,會導致后邊有個指令重排序導致的并發問題。
java中哪些類是線程安全的?concurrent 開頭的是線程安全的集合。Atomic. 開頭的也是線程安全的
private AtomicInteger atomicI = new AtomicInteger(0);private int i = 0;public static void main(String[] args) {final Counter cas = new Counter();List<Thread> ts = new ArrayList<Thread>(600);long start = System.currentTimeMillis();for (int j = 0; j < 100; j++) {Thread t = new Thread(new Runnable() {@Overridepublic void run() {for (int i = 0; i < 10000; i++) {cas.count();cas.safeCount();}}});ts.add(t);}for (Thread t : ts) {t.start();}// 等待所有線程執行完成for (Thread t : ts) {try {t.join();} catch (InterruptedException e) {e.printStackTrace();}}System.out.println(cas.i);System.out.println(cas.atomicI.get());System.out.println(System.currentTimeMillis() - start);}/** * 使用CAS實現線程安全計數器 */private void safeCount() {for (;;) {int i = atomicI.get();boolean suc = atomicI.compareAndSet(i, ++i);if (suc) {break;}}}/*** 非線程安全計數器*/private void count() {i++;}}
一個使用AtomicInteger
實現的線程安全計數器atomicI
,另一個是普通的非線程安全整數i
。在多線程環境下運行這兩個計數器,會看到不同的結果。
主要分析
-
線程安全問題:
count()
方法使用普通的i++
操作,在多線程環境下會出現競態條件(Race Condition),導致最終結果小于預期值。safeCount()
方法使用AtomicInteger
和 CAS 操作,保證了原子性,最終結果是正確的。
-
CAS 操作的實現:
?for (;;) {int i = atomicI.get();boolean suc = atomicI.compareAndSet(i, ++i);if (suc) {break;} }
這段代碼通過循環 CAS 操作來保證原子性:
- 獲取當前值
- 嘗試更新為新值
- 如果失敗則重試,直到成功
-
性能考慮:
- 原子操作雖然避免了鎖的開銷,但在高競爭環境下可能會因為頻繁重試而降低效率
- 非原子操作雖然沒有這些開銷,但結果是錯誤的,所以不能只考慮性能
輸出結果
對于 100 個線程,每個線程執行 10000 次遞增操作:
atomicI.get()
的輸出結果應為:100 * 10000 = 1,000,000i
的輸出結果通常會小于 1,000,000,因為非線程安全操作會導致部分遞增丟失- 執行時間取決于系統環境,但通常原子操作會比非原子操作慢一些
- ?我們看由于它沒有加鎖。他100萬字肯定有相互覆蓋,最終的結果肯定不到100萬,就極大概率不到100萬,有沒有看到100萬的一個極小的可能?比較大的可能是到不了100萬。因為會有相互覆蓋的問題。能理解的加九。對他100萬次調用
?ABA問題
在多線程編程中,ABA 問題(ABA Problem)是使用?CAS(Compare-and-Swap)?操作時可能遇到的一種特殊問題。它發生在一個值從 A 變為 B,然后再變回 A 的過程中,而 CAS 操作無法感知這個中間變化,誤認為值沒有被修改過。
1.?ABA 問題的本質
CAS 操作的核心邏輯是:“如果當前值是預期值 A,則將其更新為 B”。但如果在檢查期間,值經歷了?A → B → A
?的變化,CAS 會認為值 “沒有變化” 并成功更新,而實際上值已經被其他線程修改過,可能導致潛在的錯誤。
2.?ABA 問題示例
假設有一個共享變量?int value = 10
,兩個線程 T1 和 T2 同時操作它:
- T1 讀取 value:T1 準備將?
value
?從 10 增加到 11,它先讀取?value
?的當前值為 10。 - T2 修改 value:T2 先將?
value
?改為 20,然后又改回 10(即?10 → 20 → 10
)。 - T1 執行 CAS:T1 執行?
CAS(預期值=10, 新值=11)
,發現當前值確實是 10,認為沒有變化,成功將?value
?改為 11。
問題:T1 認為?value
?沒有被修改過,但實際上它經歷了?10 → 20 → 10
?的變化,可能導致 T1 的操作基于錯誤的假設(例如,T1 可能期望?value
?自從讀取后沒有被其他線程動過)。
3.?ABA 問題的危害
- 數據一致性風險:在某些業務邏輯中,中間狀態的變化可能影響最終結果。例如:
- 鏈表操作:刪除節點 A,插入新節點 B(內容與 A 相同),此時 CAS 可能誤判節點未變。
- 賬戶余額:余額從 100 變為 200 再變回 100,CAS 可能允許重復扣款。
- 邏輯錯誤:某些操作依賴值的連續性變化,而 ABA 可能破壞這種假設。
4.解決方法
解決 ABA 就加版本號就可以了
線程可見性
一個線程對共享變量做出了修改,其他線程能及時讀取到最新的修改,這叫線程可見性
?
線程間如何通信和進程間如何通信
進程是由線程組成的。進程所有的功能線程全都有。進程所擁有的功能線程全都有。然后。線程擁有的功能進程不一定有進程的通信方式線程全都有
里邊通信其實是指交換信息的意思,就把消息傳遞過去,線程的通信方式有兩種共享內存和消息傳遞
?
?
?