學習完 AQS,本文我們就來研究第一個 AQS 的實作類:ReentrantLock。
1 基本設計
ReentrantLock
可重入鎖,可重入表示同一個線程可以對同一個共享資源重複的加鎖或釋放鎖。
具有與使用 synchronized 方法和語句通路的隐式螢幕鎖相同的基本行為和語義的可重入互斥鎖,但具有擴充功能。
ReentrantLock
由最後成功鎖定但尚未解鎖的線程所擁有。當另一個線程不擁有該鎖時,調用該鎖的線程将成功傳回該鎖。如果目前線程已經擁有該鎖,則該方法将立即傳回。可以使用 isHeldByCurrentThread 和getHoldCount 方法進行檢查。
此類的構造函數接受一個可選的 fairness 參數。設定為true時,在争用下,鎖傾向于授予給等待時間最長的線程。否則,此鎖不能保證任何特定的通路順序。使用多線程通路的公平鎖的程式可能會比使用預設設定的程式呈現較低的總吞吐量(即較慢;通常要慢得多),但獲得鎖并保證沒有饑餓的時間差異較小。但是請注意,鎖的公平性不能保證線程排程的公平性。是以,使用公平鎖的多個線程之一可能會連續多次獲得它,而其他活動線程沒有進行且目前未持有該鎖。還要注意,未定時的 tryLock 方法不支援公平性設定。如果鎖可用,即使其他線程正在等待,它将成功。
建議的做法是始終立即在調用後使用try塊進行鎖定,最常見的是在構造之前/之後,例如:
class X {
private final ReentrantLock lock = new ReentrantLock();
// ...
public void m() {
lock.lock(); // block until condition holds
try {
// ... method body
} finally {
lock.unlock()
}
}
}
除了實作Lock接口之外,此類還定義了許多用于檢查鎖狀态的 public 方法和 protected 方法。 其中一些方法僅對檢測和監視有用。
此類的序列化與内置鎖的行為相同:反序列化的鎖處于解鎖狀态,而不管序列化時的狀态如何。
此鎖通過同一線程最多支援2147483647個遞歸鎖。 嘗試超過此限制會導緻鎖定方法引發錯誤。
2 類架構
- ReentrantLock 本身不繼承 AQS,而是實作了 Lock 接口
Lock 接口定義了各種加鎖,釋放鎖的方法,比如 lock() 這種不響應中斷擷取鎖,在ReentrantLock 中實作的 lock 方法是通過調用自定義的同步器 Sync 中的的同名抽象方法,再由兩種模式的子類具體實作此抽象方法來擷取鎖。
ReentrantLock 就負責實作這些接口,使用時,直接調用的也是這些方法,這些方法的底層實作都是交給 Sync 實作。
3 構造方法
-
無參數構造方法
相當于 ReentrantLock(false),預設為非公平的鎖
- 有參構造方法,可以選擇鎖的公平性
可以看出
- 公平鎖依靠 FairSync 實作
- 非公平鎖依靠 NonfairSync 實作
4 Sync 同步器
- 結構圖
- 繼承體系
可見是ReentrantLock的抽象靜态内部類 Sync 繼承了 AbstractQueuedSynchronizer ,是以ReentrantLock依靠 Sync 就持有了鎖的架構,隻需要 Sync 實作 AQS 規定的非 final 方法即可,隻交給子類 NonfairSync 和 FairSync 實作 lock 和 tryAcquire 方法
4.1 NonfairSync - 非公平鎖
- Sync 對象的非公平鎖
4.1.1 lock
- 非公平模式的 lock 方法
- 若 CAS(已經定義并實作在 AQS 中的 final 方法)state 成功,即擷取鎖成功并将目前線程設定為獨占線程
- 若 CAS state 失敗,即擷取鎖失敗,則進入 AQS 中已經定義并實作的 Acquire 方法善後
這裡的 lock 方法并沒有直接調用 AQS 提供的 acquire 方法,而是先試探地使用 CAS 擷取了一下鎖,CAS 操作失敗再調用 acquire 方法。這樣設計可以提升性能。因為可能很多時候我們能在第一次試探擷取時成功,而不需要再經過
acquire => tryAcquire => nonfairAcquire
的調用鍊。
4.1.2 tryAcquire
其中真正的實作 nonfairTryAcquire 就定義在其父類 Sync 中。下一節分析。
4.2 FairSync - 公平鎖
隻實作 lock 和 tryAcquire 兩個方法
4.2.1 lock
- 公平模式的 lock
直接調用 acquire,而沒有像非公平模式先試圖擷取,因為這樣可能導緻違反“公平”的語義:在已等待在隊列中的線程之前擷取了鎖。
acquire 是 AQS 的方法,表示先嘗試獲得鎖,失敗之後進入同步隊列阻塞等待,詳情見本專欄的上一文
4.2.2 tryAcquire
公平模式的 tryAcquire。不要授予通路權限,除非遞歸調用或沒有等待線程或是第一個調用的。
- 該方法是 AQS 在 acquire 方法中留給子類去具體實作的
話不多說,看源碼:
protected final boolean tryAcquire(int acquires) {
// 擷取目前的線程
final Thread current = Thread.currentThread();
// 擷取 state 鎖的狀态
int c = getState();
// state == 0 => 尚無線程擷取鎖
if (c == 0) {
// 判斷 AQS 的同步對列裡是否有線程等待,若沒有則直接 CAS 擷取鎖
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
// 擷取鎖成功,設定獨占線程
setExclusiveOwnerThread(current);
return true;
}
}
// 判斷已經擷取鎖是否為目前的線程
else if (current == getExclusiveOwnerThread()) {
// 鎖的重入, 即 state 加 1
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
和 Sync 的 nonfairTryAcquire 方法實作類似,唯一不同的是當發現鎖未被占用時,使用 hasQueuedPredecessors 確定了公平性。
hasQueuedPredecessors
會判斷目前線程是不是屬于同步隊列的頭節點的下一個節點(頭節點是釋放鎖的節點)
- 如果是(傳回false),符合FIFO,可以獲得鎖
- 如果不是(傳回true),則繼續等待
public final boolean hasQueuedPredecessors() {
// 這種方法的正确性取決于頭在尾之前初始化和頭初始化。如果目前線程是隊列中的第一個線程,則next是精确的
Node t = tail; // 按反初始化順序讀取字段
Node h = head;
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
5 nonfairTryAcquire
執行非公平的 tryLock。
tryAcquire 是在子類中實作的,但是都需要對trylock 方法進行非公平的嘗試。
final boolean nonfairTryAcquire(int acquires) {
// 擷取目前的線程
final Thread current = Thread.currentThread();
// 擷取 AQS 中的 state 字段
int c = getState();
// state 為 0,表示同步器的鎖尚未被持有
if (c == 0) {
// CAS state 擷取鎖(這裡可能有競争,是以可能失敗)
if (compareAndSetState(0, acquires)) {
// 擷取鎖成功, 設定擷取獨占鎖的線程
setExclusiveOwnerThread(current);
// 直接傳回 true
return true;
}
}
// 判斷現在擷取獨占鎖的線程是否為目前線程(可重入鎖的展現)
else if (current == getExclusiveOwnerThread()) {
// state 計數加1(重入擷取鎖)
int nextc = c + acquires;
if (nextc < 0) // 整型溢出
throw new Error("Maximum lock count exceeded");
// 已經擷取 lock,是以這裡不考慮并發
setState(nextc);
return true;
}
return false;
}
無參的 tryLock 調用的就是此方法
6 tryLock
6.1 無參
Lock 接口中定義的方法。
- 僅當鎖在調用時未被其他線程持有時,才擷取鎖
如果鎖未被其他線程持有,則擷取鎖,并立即傳回值 true,将鎖持有計數設定為1。即使這個鎖被設定為使用公平的排序政策,如果鎖可用,調用 tryLock() 也會立即獲得鎖,不管其他線程是否正在等待鎖。這種妥協行為在某些情況下是有用的,雖然它破壞了公平。如果想為這個鎖執行公平設定,那麼使用 tryLock(0, TimeUnit.SECONDS),這幾乎是等價的(它還可以檢測到中斷)。
如果目前線程已經持有該鎖,那麼持有計數将增加1,方法傳回true。
如果鎖被另一個線程持有,那麼這個方法将立即傳回值false。
- 典型的使用方法
Lock lock = ...;
if (lock.tryLock()) {
try {
// manipulate protected state
} finally {
lock.unlock();
}
} else {
// 執行可選的操作
}
6.2 有參
- 提供了逾時時間的入參,在時間内,仍沒有得到鎖,會傳回 false
其中的 doAcquireNanos 已經實作好在 AQS 中。
7 tryRelease
釋放鎖,對于公平和非公平鎖都适用
protected final boolean tryRelease(int releases) {
// 釋放 releases (由于可重入,這裡的 c 不一定直接為 0)
int c = getState() - releases;
// 判斷目前線程是否是擷取獨占鎖的線程
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
// 鎖已被完全釋放
if (c == 0) {
free = true;
// 無線程持有獨占鎖,是以置 null
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
8 總結
AQS 搭建了整個鎖架構,子類鎖的實作隻需要根據場景,實作 AQS 對應的方法即可。