Java基礎知識點總結
690
2025-04-11
學習完 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() } } }
1
2
3
4
5
6
7
8
9
10
11
12
13
除了實現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; }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
和 Sync 的 nonfairTryAcquire 方法實現類似,唯一不同的是當發現鎖未被占用時,使用 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()); }
1
2
3
4
5
6
7
8
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; }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
無參的 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 { // 執行可選的操作 }
1
2
3
4
5
6
7
8
9
10
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; }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
8 總結
AQS 搭建了整個鎖架構,子類鎖的實現只需要根據場景,實現 AQS 對應的方法即可。
任務調度
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。