ReentrantLock非公平鎖與公平鎖的實現
- 2020 年 1 月 2 日
- 筆記
ReentrantLock非公平鎖與公平鎖的實現
在文章開始之前,大家複習一遍鎖的分類:
ReentrantLock是根據傳入的參數來決定是否使用公平鎖,默認使用非公平鎖:
- 公平鎖/非公平鎖 當多個執行緒來取鎖的時候,按照規則排隊等鎖即為公平鎖,不按照規則排隊的即為非公平鎖, Synchronized就是一個典型的非公平鎖,而ReentrantLock 是根據AQS來實現執行緒的一個調度達到公平鎖與非公平鎖的一個切換。
- 可重入鎖 一個帶鎖的方法塊可以調用另一個帶鎖的方法塊,也就是在執行執行緒的期間,前後會用到兩個鎖,可重入鎖就有效的避免了前後死鎖的情況。
- 獨享鎖/共享鎖 獨享鎖是該鎖只能被一個執行緒所持有,共享鎖是值該鎖可以被多個執行緒所持有。 ReentrantLock和Synchronized都是獨享鎖,而ReadWriteLock中讀鎖是共享鎖,寫鎖是獨享鎖
- 樂觀鎖/悲觀鎖 悲觀鎖適合寫操作,樂觀鎖適合讀操作。
- 分段鎖 是針對部分數據設置的一種鎖,例如我要改變某一個有規則的數據鏈,我定位在某一塊,只需要鎖住這一塊的數據,就能保證整體的數據一致。 例如在 CurrentHashMap中,根據hashcode定位數據塊,實現分段鎖。
- 偏向鎖/輕量級鎖/重量級鎖
- 自旋鎖 自旋鎖是採用讓當前執行緒不停地的在循環體內執行實現的,當循環的條件被其他執行緒改變時 才能進入臨界區 可以參考:http://ifeve.com/java_lock_see1/
大家說到ReentrantLock這個鎖,一般情況下第一個想法是它是一個可沖入鎖,但是我認為另一個概念公平鎖和非公平鎖的實現更能體現出它的內涵:
//使用默認的非公平鎖ReentrantLock nonFairReentrantLock = new ReentrantLock();//構造函數入參傳true使用公平鎖ReentrantLock fairReentrantLock = new ReentrantLock(true);
判斷內容如下:
public ReentrantLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync(); }從上邊程式碼我們看到,通過構造函數中的一個布爾入參實現具體聲明公平鎖還是非公平鎖。
眾所周知ReentrantLock 底層是由AQS實現的,下圖展示了AQS的鏈表結構:


以下是非公平鎖的實現,在lock方法和tryAcquire方法中添加compareAndSetState方法,判斷當前鎖是否被佔用,如果沒有則當前執行緒會佔用鎖, 不會去判斷隊列中是否有隊列是否在等待。以下是程式碼解釋
static final class NonfairSync extends Sync { private static final long serialVersionUID = 7316153563782823691L; /** * Performs lock. Try immediate barge, backing up to normal * acquire on failure. */ final void lock() { if (compareAndSetState(0, 1)) setExclusiveOwnerThread(Thread.currentThread()); else acquire(1); } protected final boolean tryAcquire(int acquires) { return nonfairTryAcquire(acquires); } } final boolean nonfairTryAcquire(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; } } }以下是公平鎖,tryAcquire方法中判斷了隊列中的是否有執行緒等待。。
static final class FairSync extends Sync { private static final long serialVersionUID = -3000897897090466540L; final void lock() { acquire(1); } /** * Fair version of tryAcquire. Don't grant access unless * recursive call or no waiters or is first. */ 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; } }通過重入鎖的公平鎖與非公平鎖的實現,我們看到是依賴於底層AQS來實現的,所以在這簡單講解一下AQS的原理
AQS定義兩種資源共享方式:Exclusive(獨佔,只有一個執行緒執行)和share(共享,多個執行緒可同時執行,如Semaphore/CountDownLatch)
在這引用 waterystone部落格中的兩句總結:
以ReentrantLock為例,state初始化為0,表示未鎖定狀態。A執行緒lock()時,會調用tryAcquire()獨佔該鎖並將state+1。 此後,其他執行緒再tryAcquire()時就會失敗,直到A執行緒unlock()到state=0(即釋放鎖)為止,其它執行緒才有機會獲取該鎖。 當然,釋放鎖之前,A執行緒自己是可以重複獲取此鎖的(state會累加),這就是可重入的概念。但要注意,獲取多少次就要釋放多麼次,這樣才能保證state是能回到零態的。 再以CountDownLatch以例,任務分為N個子執行緒去執行,state也初始化為N(注意N要與執行緒個數一致)。這N個子執行緒是並行執行的,每個子執行緒執行完後countDown()一次,state會CAS減1。等到所有子執行緒都執行完後(即state=0),會unpark()主調用執行緒,然後主調用執行緒就會從await()函數返回,繼續後余動作。
總結:ReentrantLock通過構造參數fair來判斷是創建公平鎖還是非公平鎖,底層中的獨享鎖的實現以及隊列等待功能依賴於AQS, AQS是java中大部分鎖的基礎,其中可以劃分獨享和共享,根據volatile欄位state實現可沖入。

