synchronized
和ReentrantLock
是Java中用於執行緒同步的兩種機制。它們之間有一些相似之處,但也存在許多區別。以下是它們的對比:
共同點
目的:二者都是用於實現執行緒間的互斥與同步,以確保共享資源的安全訪問。
可重入性:兩者都是可重入鎖,這意味著一個持有鎖的執行緒可以再次獲取該鎖而不會發生死鎖。
區別
實現方式
是Java.util.concurrent包中的一個類,由Java程式碼實現。
使用AQS實現執行緒同步,以雙向佇列維護等待執行緒。
透過CAS操作+volatile變數來實現執行緒安全的鎖狀態改變。
鎖的獲取邏輯在
lock()
和tryAcquire(int arg)
中實現,釋放邏輯在unlock()
和release(int arg)
中實現。是Java語言層面的關鍵字,由JVM實現(每個物件在Java中都有一個隱式的監視器鎖 (monitor lock),
synchronized
就是基於這個鎖實現的)。基於物件頭中的
Mark Word
記錄鎖狀態,並且利用CAS操作實現鎖的競爭和釋放。JVM透過偏向鎖、輕量級鎖、自旋鎖和重量級鎖等機制最佳化效能。
編譯時固化成位元組碼,JDK層面無法直接看到其實現。
synchronized:
ReentrantLock:
使用方式
需要顯式呼叫
lock()
方法獲取鎖,unlock()
方法釋放鎖。使用起來稍微複雜,需要在
finally
塊中確保鎖的釋放以避免死鎖。簡單易用,直接透過在方法或者程式碼塊上加
synchronized
關鍵字實現。自動釋放鎖,無需顯式呼叫解鎖方法。
synchronized
:ReentrantLock
:功能特性
synchronized
每個鎖只有一個隱式的條件佇列,可以透過wait()
,notify()
,notifyAll()
來操作。ReentrantLock
支援多個條件變數,透過newCondition()
可以建立多個Condition物件,以便更加靈活地控制執行緒的等待和喚醒。synchronized
不支援超時功能。ReentrantLock
支援嘗試在指定時間內獲取鎖,使用tryLock(long timeout, TimeUnit unit)
方法。synchronized
不支援鎖獲取的中斷。ReentrantLock
支援可中斷的鎖獲取,透過lockInterruptibly()
實現。可中斷鎖獲取:
超時獲取鎖:
多條件變數:
公平鎖
synchronized
沒有內建的公平性控制。ReentrantLock
可以選擇使用公平鎖或非公平鎖,透過建構函式ReentrantLock(boolean fair)
來指定。公平鎖會按照請求的順序分配鎖,而非公平鎖則可能使某些執行緒長時間等待。效能
在低競爭的情況下,
synchronized
經過JVM的一系列最佳化,效能通常較好。ReentrantLock
由於其豐富的功能,在高競爭和複雜場景下可能更適用。
選擇建議
如果需求簡單,只是需要基本的鎖機制,並且不需要中斷、超時等高階功能,
synchronized
是一個不錯的選擇,因為它更簡單且由JVM最佳化。如果需要鎖的中斷、超時機制,以及多個條件變數等高階特性,
ReentrantLock
提供了更大的靈活性和控制力。
synchronized原始碼分析
synchronized
關鍵字是Java中的一種內建鎖機制,用於實現執行緒同步。雖然在Java程式碼中使用synchronized
非常簡單,但其底層實現涉及JVM和作業系統的互動,因此需要從Java位元組碼和JVM層面進行分析。
基本概念
synchronized
可以用來修飾方法或者程式碼塊。它的主要作用是確保在同一時刻只有一個執行緒可以執行被保護的程式碼,從而防止執行緒間的競爭條件。
同步程式碼塊
當在Java程式碼中使用synchronized
語句塊時,編譯器會將其轉化為位元組碼指令。這些位元組碼包括:
monitorenter
:表示進入同步塊,獲取鎖。monitorexit
:表示退出同步塊,釋放鎖。這通常會有兩個monitorexit
指令,一個在正常退出時,一個在異常退出時,以確保鎖的釋放。
public void synchronizedBlockExample() { synchronized(this) { // 程式碼塊 } }
編譯後生成的位元組碼類似於:
0: aload_0 // 載入"this"到棧頂 1: dup 2: monitorenter // 獲取鎖 3: ... // 被同步的程式碼塊 4: monitorexit // 釋放鎖 5: goto 13 6: astore_1 7: aload_0 8: monitorexit // 異常時釋放鎖 9: aload_1 10: athrow 11: ... 12: ... 13: ...
同步方法
對於synchronized
方法,JVM透過方法標誌位來實現同步,而不需要顯式的monitorenter
和monitorexit
指令。方法級的synchronized
是由JVM進入和退出方法時隱式處理的。
public synchronized void synchronizedMethodExample() { // 方法體 }
在位元組碼中,沒有直接的monitorenter
和monitorexit
指令,而是透過方法訪問標誌(access flags)來表明這個方法是同步的。
鎖的實現細節
synchronized
的鎖機制依賴於物件頭中的Mark Word來儲存鎖狀態。根據鎖的不同狀態,Mark Word可能包含以下資訊:
無鎖:用於儲存物件的雜湊碼、分代年齡等。
偏向鎖:儲存執行緒ID,表示鎖被某個執行緒持有但沒有發生競爭。
輕量級鎖:儲存指向棧幀的指標,用於快速獲取鎖。
重量級鎖:使用作業系統的Mutex來管理鎖。
鎖最佳化
爲了提高效能,JVM進行了多種鎖最佳化:
偏向鎖:如果多次看到相同的執行緒請求鎖,則認為該執行緒會再次請求,從而減少不必要的同步開銷。
輕量級鎖:在無競爭情況下,透過CAS操作避免使用重量級鎖。
自旋鎖:在短時間鎖爭用情況下,允許執行緒自旋等待而不是阻塞,以減少執行緒切換的開銷。
ReentrantLock原始碼分析
ReentrantLock
是Java併發包(java.util.concurrent.locks)中的一個鎖實現。它提供了比synchronized
關鍵字更靈活和強大的功能,比如可中斷的鎖獲取、超時獲取鎖以及多個條件變數等。下面是對ReentrantLock
核心原始碼的一些分析。
核心元件
ReentrantLock
的核心是基於AbstractQueuedSynchronizer
(AQS)來實現同步控制。AQS是一個用於構建鎖和其他同步元件的框架。
AQS概述
狀態欄位:使用一個
int
型別的state
欄位來表示鎖的狀態。FIFO佇列:使用一個FIFO等待佇列來管理處於等待狀態的執行緒。
模板方法:透過繼承AQS,實現特定資源的獲取和釋放邏輯。
ReentrantLock的實現細節
基本結構
ReentrantLock
有兩種模式:公平模式和非公平模式,分別由兩個內部靜態類FairSync
和NonfairSync
實現,這兩個類都繼承自AQS:
public class ReentrantLock implements Lock, java.io.Serializable { private final Sync sync; abstract static class Sync extends AbstractQueuedSynchronizer { // 實現具體的鎖機制 } static final class NonfairSync extends Sync { // 非公平鎖實現 } static final class FairSync extends Sync { // 公平鎖實現 } public ReentrantLock() { sync = new NonfairSync(); // 預設非公平鎖 } public ReentrantLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync(); } }
鎖的獲取
非公平鎖(NonfairSync)
protected final boolean tryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) { if (compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } else if (current == getExclusiveOwnerThread()) { int nextc = c + acquires; if (nextc < 0) // overflow throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } return false; }
在非公平鎖中,嘗試獲取鎖時,如果沒有執行緒持有鎖,則直接透過CAS設定狀態為已獲取;如果當前執行緒已經持有鎖,則增加重入次數。
公平鎖(FairSync)
protected final boolean tryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) { if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } else if (current == getExclusiveOwnerThread()) { int nextc = c + acquires; if (nextc < 0) throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } return false; }
在公平鎖中,除了檢查鎖狀態外,還會檢查是否有其他執行緒在排隊等待獲取鎖,這樣可以保證先請求鎖的執行緒優先獲得鎖。
鎖的釋放
protected final boolean tryRelease(int releases) { int c = getState() - releases; if (Thread.currentThread() != getExclusiveOwnerThread()) throw new IllegalMonitorStateException(); boolean free = false; if (c == 0) { free = true; setExclusiveOwnerThread(null); } setState(c); return free; }
鎖的釋放邏輯主要是減少持有計數,並在計數為0時釋放鎖(清空持有執行緒)。
條件變數
ReentrantLock
支援條件變數,透過內部的ConditionObject
實現:
public Condition newCondition() { return sync.newCondition(); } final class ConditionObject implements Condition, java.io.Serializable { // 實現條件變數的等待和通知機制 }
條件變數允許執行緒在特定條件下等待並被喚醒,這是synchronized
無法直接實現的多條件等待模型。
非公平鎖搶佔機制
在ReentrantLock
的非公平鎖場景下,當多個執行緒在等待獲取同一把鎖時,具體哪個執行緒最終能夠拿到鎖並不是嚴格確定的,因為非公平鎖機制允許一定程度的“插隊”。下面是詳細的流程分析:
基本概念
非公平性:非公平鎖不保證按照請求的順序分配鎖,新加入的執行緒會直接嘗試獲取鎖而不考慮等待佇列。
詳細流程
初始狀態
假設有若干個執行緒
T1
,T2
, ...,Tn
在爭奪同一個ReentrantLock
。當鎖被某個執行緒持有時,例如
T0
,其他執行緒會進入AQS(AbstractQueuedSynchronizer)的等待佇列。執行緒嘗試獲取鎖
新加入的執行緒(例如
Tnew
)呼叫lock()
方法。在非公平鎖的模式下,執行緒首先透過CAS操作嘗試立即獲取鎖,不管等待佇列中是否有其他執行緒。
CAS操作
成功:如果
compareAndSetState(0, 1)
成功,即鎖當前是空閒的,那麼Tnew
獲得鎖,成為持有鎖的執行緒。失敗:如果CAS操作失敗,這意味著另一執行緒持有鎖或在同一時刻成功獲取了鎖。
進入等待佇列
如果CAS失敗,
Tnew
和其他未能獲取鎖的執行緒將進入AQS的等待佇列。AQS使用一個FIFO佇列來管理這些執行緒。
佇列中的競爭
即使在等待佇列中,非公平鎖仍然允許新來的執行緒嘗試插隊獲取鎖。如果鎖在這個過程中被釋放,新執行緒可能直接透過CAS操作成功獲取鎖。
由於CAS操作是原子的,並且多個執行緒會同時爭奪鎖,最終成功的執行緒(無論是否在佇列中)將是最先完成CAS設定的那個執行緒。
鎖的釋放與重新競爭
當前持有鎖的執行緒釋放鎖時,會呼叫
unlock()
,這將設定狀態為0,並喚醒等待佇列中的頭節點。被喚醒的執行緒將從等待佇列中出隊,並嘗試獲取鎖。然而,由於非公平特性,任何其他執行緒也可在此時嘗試獲取鎖。
結果不確定性
雖然通常喚醒的執行緒有更高的機會拿到鎖,但並不絕對。任何處於執行態的新執行緒或已經喚醒的執行緒都有機會在鎖釋放後立即嘗試獲取鎖。
總結
在非公平鎖的環境中,哪個執行緒最終獲得鎖是由多個因素決定的,包括執行緒排程、CPU時間片以及CAS操作的競爭結果。由於新的執行緒可以在任何時候嘗試獲取鎖,所以存在較大的不確定性。這種策略提高了併發效能,但可能導致某些執行緒長時間得不到鎖,這就是所謂的“鎖飢餓”現象。
可重入性實現原理
synchronized的可重入性
synchronized
是Java內建關鍵字,用於簡化鎖管理,其可重入性由JVM層面直接支援。
監視器鎖:
每個物件在Java中都有一個隱式的監視器鎖 (monitor lock),
synchronized
就是基於這個鎖實現的。當一個執行緒訪問被
synchronized
修飾的方法或程式碼塊時,它會嘗試獲取該物件的監視器鎖。鎖計數器與持有執行緒:
JVM在內部透過關聯一個鎖計數器(lock count)和持有執行緒(owner)來管理可重入性。
當一個執行緒第一次獲得鎖時,計數器加一,並記錄該執行緒為持有者。
如果同一個執行緒再次進入同步程式碼塊/方法,計數器會繼續增加,而不會阻塞,因為持有者是自己。
鎖釋放:
每次退出同步程式碼塊/方法時,計數器減一。
當計數器為零時,監視器鎖才真正釋放,允許其他執行緒獲取。
ReentrantLock的可重入性
ReentrantLock
是Java提供的明文鎖,位於java.util.concurrent.locks
包中,由AbstractQueuedSynchronizer
(AQS) 支援其複雜功能。
AQS框架:
ReentrantLock
利用AQS
中的狀態(state)欄位來實現鎖的計數,與持有執行緒資訊一起管理可重入性。鎖獲取與持有執行緒:
當執行緒第一次獲取鎖時,AQS的
state
欄位從0變為1,並記錄當前執行緒為持有執行緒。如果持有該鎖的執行緒再次請求鎖,
state
欄位遞增,而不需要重新獲取鎖。鎖釋放:
執行緒每釋放一次鎖,
state
欄位遞減。當
state
降至0時,持有執行緒資訊清空,鎖完全釋放,可以被其他執行緒獲取。顯式控制:
與
synchronized
不同,ReentrantLock
提供了顯式的鎖控制方法,如lock()
、unlock()
、tryLock()
等,使得程式設計師可以更靈活地管理鎖。