文章目錄
- Java多線程進階:死鎖與面試題解析
- 一、并發編程的噩夢——死鎖
- 1. 什么是死鎖?四個缺一不可的條件
- 2. 如何避免死鎖?從破壞循環等待開始
- 二、并發編程面試題全景解析
- 1. 鎖與同步機制
- 2. CAS 與原子操作
- 3. JUC 工具與線程池
- 4. 線程安全集合
- 5. 綜合問題
- 本文核心要點總結 (Key Takeaways)
Java多線程進階:死鎖與面試題解析
學到這里,我們總算來到了 Java 多線程學習之旅的最后一站。在前面的筆記里,我們一起探索了鎖策略、synchronized
的底層細節、JUC 工具包以及各種并發集合。現在,是時候挑戰兩個“終極 BOSS”了:
- 死鎖:并發編程中最讓人頭疼的噩夢。
- 面試題:檢驗我們學習成果的試金石。
這篇筆記的目標很明確:首先,徹底搞懂死鎖的成因和避免策略;然后,整理一份相對全面的并發面試題庫。這既是對整個系列知識的最終鞏固,也希望能幫助自己(以及其他可能讀到這篇筆記的朋友)更自信地應對面試的挑戰。
一、并發編程的噩夢——死鎖
1. 什么是死鎖?四個缺一不可的條件
死鎖,簡單來說,就是多個線程同時被阻塞,它們中的一個或全部都在等待對方持有的資源。因為大家都在等對方先放手,結果就導致所有線程都被無限期地阻塞,程序也就卡住無法正常結束了。
這里我的理解是,可以用一個“吃餃子”的場景來幫助消化這個概念:
滑稽老哥和不吃香菜一起去吃餃子,但桌上只有一瓶醬油和一瓶醋。
- 滑稽老哥手快,先拿起了醬油瓶(這相當于持有了鎖 A)。
- 與此同時,不吃香菜拿起了醋瓶(持有了鎖 B)。
接下來,滑稽老哥想蘸醋(請求鎖 B),而不吃香菜想用醬油(請求鎖 A)。如果兩人誰也不肯先把自己的瓶子放下給對方用,那就僵持住了——這就是一個典型的死鎖。
從這個例子可以總結出,死鎖的產生必須同時滿足以下四個缺一不可的條件:
- 互斥使用:一個資源一次只能被一個線程使用(醬油瓶一次只能一人拿)。
- 不可搶占:線程不能強行從另一個線程手中奪取資源,只能等待其主動釋放(不能從對方手里搶瓶子)。
- 請求和保持:線程在持有至少一個資源的同時,又去請求其他資源(拿著醬油瓶,又想要醋瓶)。
- 循環等待:存在一個線程等待鏈,T1等T2,T2等T3,…,Tn等T1,形成環路(滑稽老哥等不吃香菜,不吃香菜又在等滑稽老哥)。
2. 如何避免死鎖?從破壞循環等待開始
要避免死鎖,理論上只需破壞上述四個條件中的任意一個即可。但在實際編程中,我們最容易、也最常入手的是破壞“循環等待”條件。
最常用的一種死鎖預防技術就是鎖排序。這個方法思路很簡單:假設有 N 個線程嘗試獲取 M 把鎖, 我們可以對這 M 把鎖進行統一編號 (例如根據它們的 hashCode
)。然后定下規矩,所有線程在需要獲取多把鎖時,都必須嚴格按照固定的、從小到大的編號順序來獲取。這樣一來,請求鎖的方向都是一致的,就從根本上避免了形成等待環路。
可能產生死lock的代碼:
下面這個例子很典型,兩個線程以相反的順序申請鎖,就可能導致死鎖。
Object lock1 = new Object();
Object lock2 = new Object();// 線程1: 嘗試先鎖 lock1,再鎖 lock2
new Thread(() -> {synchronized (lock1) {System.out.println("線程1 持有 lock1, 嘗試獲取 lock2...");try { Thread.sleep(100); } catch (InterruptedException e) {}synchronized (lock2) {System.out.println("線程1 成功獲取兩把鎖");}}
}).start();// 線程2: 嘗試先鎖 lock2,再鎖 lock1 (順序相反,非常危險!)
new Thread(() -> {synchronized (lock2) {System.out.println("線程2 持有 lock2, 嘗試獲取 lock1...");synchronized (lock1) {System.out.println("線程2 成功獲取兩把鎖");}}
}).start();
破壞循環等待后的安全代碼:
只要我們約定好,所有線程都先獲取 lock1
, 再獲取 lock2
,問題就解決了。
Object lock1 = new Object();
Object lock2 = new Object();// 兩個線程都遵循先鎖1再鎖2的順序
new Thread(() -> {synchronized (lock1) {synchronized (lock2) {System.out.println("線程1 成功");}}
}).start();new Thread(() -> {synchronized (lock1) {synchronized (lock2) {System.out.println("線程2 成功");}}
}).start();
二、并發編程面試題全景解析
這部分內容既是對整個多線程系列學習的復習,也是對常見面試問題的檢驗。
1. 鎖與同步機制
1. 如何理解樂觀鎖和悲觀鎖?具體如何實現?
- 悲觀鎖:我的理解是,它非常“悲觀”,總是假設會發生并發沖突。所以,在每次對數據進行操作前,它都會先加鎖,確保在自己操作的這段時間里,別人碰不了數據。這種方式的實現,通常依賴于底層的同步機制,比如 Java 中的
synchronized
關鍵字和ReentrantLock
類。 - 樂觀鎖:它則非常“樂觀”,總是假設不會發生并發沖突。所以,它操作數據時不加鎖,而是在準備提交更新的時候,才去檢查數據在操作期間有沒有被其他線程修改過。如果沒被改過,就成功更新;如果被改了,就放棄或者重試。它的典型實現是 CAS(Compare-and-Swap)機制,為了防止 ABA 問題,通常還會配合一個版本號字段來一起檢查。
2. 介紹一下讀寫鎖?
讀寫鎖(ReadWriteLock
)是一種很巧妙的鎖,它把鎖的功能一分為二:分為“讀鎖”和“寫鎖”,特別適合用在“讀多寫少”的場景。
- 讀鎖 vs 讀鎖:不互斥。大家都是讀數據,不會有沖突,所以允許多個線程同時持有讀鎖,并發讀取。
- 寫鎖 vs 寫鎖:互斥。為了保證數據寫入的原子性,一次只能有一個線程持有寫鎖。
- 讀鎖 vs 寫鎖:互斥。當有線程在寫數據時,其他線程不能讀,反之亦然。這保證了讀線程不會讀到修改了一半的“臟數據”。
Java 中的ReentrantReadWriteLock
就是它的標準實現。
3. 什么是自旋鎖?為什么要使用它,缺點是什么?
- 什么是自旋鎖:當一個線程想獲取鎖但發現鎖被占用了,它不會立刻放棄 CPU 進入阻塞狀態,而是在原地進行一個忙等待的循環(“自旋”),不斷地嘗試獲取鎖。
- 為什么使用:它基于一個假設——鎖被占用的時間通常很短。如果這個假設成立,那么通過自旋等待,線程可以避免進入和退出阻塞狀態的巨大開銷(這涉及到用戶態和內核態的切換以及線程調度),一旦鎖被釋放,就能立刻搶到,響應速度更快。
- 缺點:如果鎖被占用的時間很長,自旋的線程就會持續空轉,白白浪費 CPU 資源。所以它是個雙刃劍,用對地方才高效。
4. synchronized
是可重入鎖嗎?
是的,必須是。可重入鎖(也叫遞歸鎖)指的是,同一個線程在已經持有鎖的情況下,可以再次成功獲取該鎖,而不會自己把自己鎖死。
它的內部實現機制大致是:鎖本身會記錄下當前是哪個線程持有著它,并且還有一個計數器。當這個線程再次請求這把鎖時,計數器會遞增。每次執行完一次同步代碼塊釋放鎖時,計數器會遞減。只有當計數器減到 0 時,這個鎖才會被真正釋放,其他線程才能獲取。
5. 什么是偏向鎖?
偏向鎖是 JVM 對 synchronized
的一種極致優化。它的核心思想是,在大多數情況下,鎖不僅不存在多線程競爭,而且總是由同一個線程多次獲得。
為了處理這種情況,偏向鎖并不會真的加鎖,而是在對象頭里記錄一個“偏向”的線程 ID。如果后續訪問該鎖的始終是這一個線程,那么它進出同步塊時就幾乎沒有任何開銷,連 CAS 操作都不需要。這大大降低了無競爭情況下的同步成本。當然,一旦有其他線程參與競爭,偏向鎖狀態就會被撤銷,升級為輕量級鎖。
6. synchronized
的實現原理是什么?
synchronized
的實現核心是一個從偏向鎖、輕量級鎖到重量級鎖的鎖升級過程,這是 JVM 為了在不同競爭情況下都能有較好性能而做的優化。
- 偏向鎖:當沒有競爭時,鎖會“偏向”于第一個獲取它的線程。此時,僅在對象頭記錄線程ID,開銷最低。
- 輕量級鎖:當出現另一個線程競爭時,偏向鎖會升級為輕量級鎖。這時,線程會通過自旋+CAS的方式嘗試獲取鎖,避免了線程阻塞帶來的開銷。
- 重量級鎖:如果自旋一定次數后,競爭依然激烈,鎖就會“膨脹”為重量級鎖。此時它會依賴操作系統的
mutex
互斥量來實現。獲取不到鎖的線程會被掛起,進入等待隊列,等待操作系統喚醒。
除此之外,JVM 還會進行鎖消除(如果判斷一段代碼不可能有共享數據競爭,就直接去掉鎖)和鎖粗化(將多個連續的加解鎖操作合并為一個更大的鎖)等優化。
2. CAS 與原子操作
1. 講解一下你自己理解的 CAS 機制。
我理解的 CAS(Compare-and-Swap,比較并交換)是一種非常底層的原子操作,很多無鎖編程都依賴它。你可以把它想象成一個需要三個參數的指令:
- 要操作的內存地址 V
- 我預期的這個地址上的舊值 A
- 我想要更新成的新值 B
執行這條指令時,CPU 會原子性地做一件事:檢查地址 V 上的當前值是否等于我預期的舊值 A。當且僅當它倆相等時,才會把地址 V 上的值更新為新值 B。整個“比較再更新”的過程是一步完成的,不會被其他線程中斷,這就是它實現原子性的關鍵。
2. ABA問題怎么解決?
ABA 問題是 CAS 的一個經典漏洞。意思是,一個值原來是 A,被其他線程改成了 B,然后又改回了 A。我的 CAS 操作在檢查時,會發現值仍然是 A,就誤以為它沒變過,然后執行更新。
要解決這個問題,光比較值是不夠的,還需要引入一個版本號或時間戳。每次更新值的時候,都把版本號加一。這樣,CAS 操作就變成了比較“值 + 版本號”。
- 原來的流程:
A -> B -> A
- 加入版本號后:
(A, v1) -> (B, v2) -> (A, v3)
當我拿著(A, v1)
去做 CAS 時,發現現在是(A, v3)
,版本號對不上,就知道中間發生過變化,從而避免了錯誤的操作。Java 中的AtomicStampedReference
類就是用來解決這個問題的。
3. AtomicInteger
的實現原理是什么?
它的核心原理就是基于 CAS 的自旋循環。以 incrementAndGet()
方法為例,它內部并沒有用鎖,而是執行了這樣一個循環:
- 讀取:先讀取
AtomicInteger
內部volatile
修飾的當前值(oldValue
)。 - 計算:在本地計算出新值(
newValue = oldValue + 1
)。 - 交換 (CAS):調用底層的
compareAndSet(oldValue, newValue)
方法,嘗試原子性地用newValue
替換oldValue
。 - 判斷與重試:如果
compareAndSet
返回true
,說明在我計算的這段時間里,沒有其他線程修改過值,更新成功,退出循環。如果返回false
,說明值已經被其他線程改了,我的oldValue
已經過時了。這時,循環會繼續,重新執行第 1 步,直到 CAS 操作成功為止。
3. JUC 工具與線程池
1. 線程同步的方式有哪些?
我目前學到的主要有這幾種:
- 最基礎的:使用
synchronized
關鍵字,簡單粗暴。 - 更靈活的:使用 JUC 包下的
Lock
接口實現,比如ReentrantLock
。 - 控制協作的:使用 JUC 包下的一些同步工具類,比如
Semaphore
(信號量)、CountDownLatch
(倒計時門閂)等。 - 無鎖方式:使用原子類,比如
AtomicInteger
,通過 CAS 保證單個變量操作的線程安全。
2. 為什么有了 synchronized
還需要 JUC 下的 Lock
?
因為以 ReentrantLock
為代表的 Lock
接口,提供了 synchronized
不具備的、更高級和更靈活的功能,讓我們可以對鎖進行更精細的控制:
- 可中斷的鎖獲取:
lockInterruptibly()
允許在等待鎖的過程中響應中斷。 - 可限時的鎖獲取:
tryLock(time, unit)
可以嘗試在指定時間內獲取鎖,超時就放棄,避免死等。 - 公平性選擇:可以在創建時指定為公平鎖,保證線程先來先得,避免饑餓。
- 靈活的線程通信:可以綁定多個
Condition
對象,實現對不同條件的線程進行分組等待和精確喚醒,而synchronized
只有一個等待隊列。
3. 信號量聽說過嗎?之前都用在過哪些場景下?
信號量(Semaphore
)我理解它是一個控制“許可證”數量的計數器,用來限制能同時訪問某個特定資源的線程數量。
acquire()
:線程嘗試獲取一個許可證,成功則計數器減一。如果許可證發完了(計數器為0),線程就得阻塞等待。release()
:線程在用完資源后,釋放一個許可證,計數器加一,可能會喚醒一個正在等待的線程。
它最常見的用途就是流量控制或資源池管理。比如,我們有一個數據庫連接池,里面只有10個連接,那就可以用一個初始值為10的 Semaphore
來控制,確保最多只有10個線程能同時拿到連接。
4. 解釋一下 ThreadPoolExecutor
構造方法的參數的含義。
這是創建線程池最核心的構造方法,每個參數都得搞清楚:
corePoolSize
: 核心線程數。線程池創建后,即使線程是空閑的,也會長期保留這么多線程。maximumPoolSize
: 最大線程數。當任務隊列滿了,線程池最多能再創建多少個“臨時”線程,總線程數不能超過這個值。keepAliveTime
: 臨時線程的空閑存活時間。當線程數超過corePoolSize
時,那些多出來的臨時線程如果空閑了這么久還沒接到新任務,就會被銷毀。unit
:keepAliveTime
的時間單位。workQueue
: 任務阻塞隊列。當核心線程都在忙時,新來的任務會先被存放在這個隊列里排隊。threadFactory
: 線程工廠。用來創建新線程,可以自定義線程的名字、是否為守護線程等。handler
: 拒絕策略。當任務隊列已滿,并且線程數也達到了maximumPoolSize
時,用來處理新提交任務的策略(比如拋異常、丟棄任務等)。
5. Java創建線程池的接口是什么?參數LinkedBlockingQueue
的作用是什么?
- 創建線程池的核心接口是
ExecutorService
。我們通常不直接實現它,而是通過Executors
這個工廠類提供的靜態方法(如newFixedThreadPool
)或者直接構造ThreadPoolExecutor
類的實例來創建。 LinkedBlockingQueue
在線程池中通常被用作任務隊列(workQueue)。它的特點是一個無界的鏈式阻塞隊列。當所有核心線程都在忙碌時,新提交的任務就會被無限地添加到這個隊列中,等待核心線程空閑后前來領取并執行。
4. 線程安全集合
1. ConcurrentHashMap
的讀是否要加鎖,為什么?
讀操作(get
方法)幾乎不加鎖,這也是它性能高的關鍵之一。
ConcurrentHashMap
為了最大化讀操作的并發性能,采取了非常精巧的設計。它通過將共享變量(比如哈希桶中 Node 節點的 val
和 next
指針)聲明為 volatile
,來利用 volatile
的內存可見性語義。這確保了當一個線程修改了某個節點后,這個修改能立刻對其他讀線程可見。這樣,讀線程總能看到最新的數據,從而實現了高效的無鎖讀取。
2. 介紹下 ConcurrentHashMap
的鎖分段技術?
鎖分段技術是 Java 1.7 版本中 ConcurrentHashMap
提高并發度的核心策略。它的思路是,不對整個哈希表加一把大鎖,而是將整個表在邏輯上分成若干個“段”(Segment),每個 Segment 本身就像一個小型的、線程安全的 Hashtable
,擁有自己獨立的鎖。
當需要操作某個 key 時,只需根據 key 的哈希值定位到它所屬的那個 Segment,然后只鎖定這一個 Segment 即可,其他 Segment 的操作完全不受影響。這相當于把一把大鎖拆分成了多把小鎖,大大提高了并發寫入的效率。
不過這里有個重點需要記一下:這個技術已經在 Java 1.8 中被優化替換了。在 Java 1.8 及以后的版本中,ConcurrentHashMap
取消了 Segment 的設計,改為使用 synchronized
鎖住哈希桶的頭節點,再加上 CAS 操作來輔助,實現了粒度更細的鎖定,并發性能通常更好。
3. Hashtable
、HashMap
、ConcurrentHashMap
之間的區別?
這三個是面試老朋友了,它們的關鍵區別在于線程安全和性能:
HashMap
: 線程不安全。如果用在多線程環境下,需要手動在外部加鎖。它的優點是性能最高,并且 key 和 value 都允許為null
。Hashtable
: 線程安全。它實現安全的方式非常粗暴,就是給幾乎所有public
方法都加上了synchronized
,鎖住的是整個Hashtable
對象。這導致并發效率極低,現在基本不推薦使用了。另外,它的 key 和 value 都不允許為null
。ConcurrentHashMap
: 線程安全,并且是為高并發場景設計的。在 JDK 1.8 中,它通過synchronized
鎖住哈希桶的頭節點 + CAS +volatile
變量來保證安全,實現了細粒度的鎖,并發性能非常高。和Hashtable
一樣,它的 key 和 value 也都不允許為null
。
5. 綜合問題
1. 談談死鎖是什么,如何避免死鎖?
- 死鎖定義:我理解的死鎖,就是兩個或多個線程在執行過程中,因為爭奪資源而陷入一種互相等待的僵局。如果沒有外力干涉,它們誰也無法繼續往下執行。
- 四個必要條件:互斥使用、不可搶占、請求與保持、循環等待。這四個條件必須同時滿足才會發生死鎖。
- 如何避免:最核心的方法就是想辦法破壞這四個必要條件之一。在編程實踐中,我們最常用的手段是通過鎖排序來破壞“循環等待”條件,即規定好所有線程都必須以一個固定的、相同的順序來獲取一系列的鎖。
2. volatile
關鍵字的用法?
volatile
關鍵字在我看來主要有兩個關鍵作用:
- 保證內存可見性:這是它最核心的功能。它能確保一個線程對
volatile
變量的修改,能夠立刻被其他線程“看到”。底層原理是它會強制線程每次都從主內存中讀取變量的值,而不是依賴自己工作內存中的緩存。 - 禁止指令重排序:
volatile
還能充當一個內存屏障,防止編譯器和處理器為了優化性能而隨意改變代碼的執行順序。這一點在實現某些底層并發算法時至關重要,比如經典的雙重檢查鎖定(DCL)單例模式。
3. Java多線程是如何實現數據共享的?
JVM 的內存模型把內存分成了幾個區域,其中**堆內存(Heap)和方法區(Method Area)**是所有線程共享的區域。所以,只要我們把一個對象(無論是類的實例還是靜態變量)創建在這兩個共享區域里,那么這個對象的引用就可以被多個線程同時持有和訪問,這樣就實現了線程間的數據共享。
4. Java線程共有幾種狀態?狀態之間怎么切換的?
根據 Thread.State
枚舉,Java 線程有 6 種狀態:
NEW
(新建): 線程對象被創建,但還沒調用start()
方法。RUNNABLE
(可運行): 這是個復合狀態,包含了操作系統線程狀態中的“就緒”(Ready)和“運行中”(Running)。調用start()
后線程就進入這個狀態,等待 CPU 調度。BLOCKED
(阻塞): 線程在等待進入一個synchronized
同步塊時,因為獲取不到監視器鎖而被阻塞。WAITING
(無限期等待): 線程需要等待其他線程執行特定的喚醒動作。調用Object.wait()
、Thread.join()
、LockSupport.park()
會進入此狀態。TIMED_WAITING
(限時等待): 和WAITING
類似,但它不會無限等下去,會在指定時間后自動喚醒。調用Thread.sleep(time)
、Object.wait(time)
等方法會進入此狀態。TERMINATED
(終止): 線程的run()
方法執行完畢,線程生命周期結束。
5. 在多線程下,如果對一個數進行疊加,該怎么做?
要保證線程安全,主要有兩種方法:
- 加鎖:最直接的方法,使用
synchronized
關鍵字或ReentrantLock
來保護這個疊加操作,確保同一時間只有一個線程能執行i++
。 - 使用原子類:更推薦的方式是使用
java.util.concurrent.atomic
包下的原子類,比如AtomicInteger
。它的addAndGet()
或incrementAndGet()
方法是基于 CAS 操作實現的,屬于無鎖操作,在并發量大的情況下性能通常比加鎖要好。
6. Servlet是否是線程安全的?
Servlet 本身的設計是單實例多線程的。也就是說,Web 容器(比如 Tomcat)通常只會為每個 Servlet 類創建一個實例。當多個 HTTP 請求同時訪問這個 Servlet 時,容器會為每個請求分配一個獨立的線程,這些線程會并發地執行同一個 Servlet 實例的 service()
方法。
因此,結論是:
- 如果 Servlet 中定義了成員變量(實例變量),并且
service()
方法對這些成員變量進行了讀寫操作,那么就會存在線程安全問題。 - 如果 Servlet 中只使用了局部變量(在
service()
方法內部定義的變量),那么它是線程安全的,因為每個線程都有自己獨立的棧空間來存放局部變量。
7. Thread
和Runnable
的區別和聯系?
Runnable
是一個接口,里面只有一個run()
方法。它代表的是一個“任務”或“要做什么事”。Thread
是一個類,它代表一個執行任務的線程實體,是真正“干活的人”。- 聯系:
Thread
類本身也實現了Runnable
接口。創建線程時,可以將一個Runnable
對象作為任務傳遞給Thread
的構造函數。 - 區別與選擇: 推薦使用實現
Runnable
接口的方式。因為它將“任務”和“執行者”解耦了,一個Runnable
任務可以被不同的Thread
執行。而如果通過繼承Thread
類的方式,由于 Java 的單繼承限制,這個類就不能再繼承其他類了,靈活性較差。
8. 多次start
一個線程會怎么樣?
一個 Thread
對象的 start()
方法只能被調用一次。
- 第一次調用
start()
會成功啟動線程,使其進入RUNNABLE
狀態,并由 JVM 調度執行其run()
方法。 - 之后對同一個線程對象再次調用
start()
,無論線程是否已經執行完畢,都會拋出IllegalThreadStateException
異常。
9. 有synchronized
兩個方法,兩個線程分別同時調用,請問會發生什么?
這要看 synchronized
修飾的是什么類型的方法,以及兩個線程調用的是否是同一個對象的實例方法。
- 修飾非靜態方法:此時鎖是當前對象實例 (
this
)。- 如果兩個線程調用的是同一個對象的這兩個同步方法,它們會互斥,一個線程執行時另一個必須等待鎖。
- 如果兩個線程調用的是不同對象的這兩個同步方法,它們之間沒有關系,不會互斥,可以并發執行。
- 修飾靜態方法:此時鎖是當前類的 Class 對象 (
Xxx.class
)。- 這種情況下,鎖是全局唯一的。無論兩個線程操作的是不是同一個對象實例,只要它們調用的是這個類的任何靜態
synchronized
方法,都會互斥。
- 這種情況下,鎖是全局唯一的。無論兩個線程操作的是不是同一個對象實例,只要它們調用的是這個類的任何靜態
10. 進程和線程的區別?
這是個基礎但非常重要的問題,我的理解是:
- 資源單位 vs 調度單位:進程是操作系統進行資源分配的最小單位(比如內存空間)。線程是 CPU 調度的最小單位,是真正執行計算的。
- 包含關系:一個進程可以包含一個或多個線程。線程是進程的一部分,不能獨立存在。
- 內存共享:進程之間的內存空間是相互獨立的。而同一進程內的所有線程共享該進程的內存空間(如堆、方法區),但每個線程有自己獨立的棧和程序計數器。
- 開銷:創建、銷毀和切換進程的開銷遠大于線程,因此線程也被稱為“輕量級進程”。
本文核心要點總結 (Key Takeaways)
- 鎖的本質是權衡:并發編程中的所有鎖策略,都是在性能開銷與數據安全之間尋找平衡點。沒有普適的“最優解”,只有“最適合”特定場景的方案。
synchronized
是智能的:現代 JVM 中的synchronized
遠非一個簡單的互斥鎖,它內部集成了偏向鎖、輕量級鎖(自旋)、重量級鎖的動態升級機制,以及鎖消除、鎖粗化等優化,努力在各種場景下都提供接近最優的性能。- CAS是無鎖并發的基石:CAS(比較并交換)作為一種 CPU 級別的原子指令,是 JUC 中許多高性能并發類(如
AtomicInteger
,ConcurrentHashMap
)的實現基礎,它用樂觀的非阻塞方式,在很多場景下替代了傳統的悲觀阻塞式加鎖。 - JUC是并發編程的瑞士軍刀:
java.util.concurrent
包提供了一套豐富、模塊化的并發工具(如ReentrantLock
,Semaphore
,CountDownLatch
, 線程池等),它們是解決復雜并發問題的強大武器庫。 - 死鎖可防可控:深刻理解死鎖產生的四個必要條件,并在編碼實踐中(最常見的是通過“鎖排序”)主動破壞其中之一,是預防這個嚴重并發問題的關鍵。
是 JUC 中許多高性能并發類(如AtomicInteger
,ConcurrentHashMap
)的實現基礎,它用樂觀的非阻塞方式,在很多場景下替代了傳統的悲觀阻塞式加鎖。 - JUC是并發編程的瑞士軍刀:
java.util.concurrent
包提供了一套豐富、模塊化的并發工具(如ReentrantLock
,Semaphore
,CountDownLatch
, 線程池等),它們是解決復雜并發問題的強大武器庫。 - 死鎖可防可控:深刻理解死鎖產生的四個必要條件,并在編碼實踐中(最常見的是通過“鎖排序”)主動破壞其中之一,是預防這個嚴重并發問題的關鍵。