您好,登錄后才能下訂單哦!
這篇文章主要講解了“JUC的ReentrantLock怎么使用”,文中的講解內容簡單清晰,易于學習與理解,下面請大家跟著小編的思路慢慢深入,一起來研究和學習“JUC的ReentrantLock怎么使用”吧!
ReentrantLock 譯為可重入鎖,我們在使用時總是將其與 synchronized 關鍵字進行對比,實際上 ReentrantLock 與 synchronized 關鍵字在使用上具備相同的語義,區別僅在于 ReentrantLock 相對于 synchronized 關鍵字留給開發者的可操作性更強,所以在使用上更加靈活,當然凡事都有兩面,靈活的背后也暗藏著更加容易出錯的風險。
盡管語義相同,但 ReentrantLock 和 synchronized 關鍵字背后的實現機制卻大相徑庭。前面的文章中我們分析了 synchronized 關鍵字的實現內幕,知道了 synchronized 關鍵字背后依賴于 monitor 技術,而本文所要分析的 ReentrantLock 在實現上則依賴于 AQS 隊列同步器,具體如何基于 AQS 進行實現,下面來一探究竟。
本小節使用 ReentrantLock 實現一個 3 線程交替打印的程序,演示基于 ReentrantLock 實現鎖的獲取、釋放,以及線程之間的通知機制。示例實現如下:
private static Lock lock = new ReentrantLock(true); private static Condition ca = lock.newCondition(); private static Condition cb = lock.newCondition(); private static Condition cc = lock.newCondition(); private static volatile int idx = 0; private static class A implements Runnable { @Override public void run() { try { lock.lock(); for (int i = 0; i < 10; i++) { cb.signalAll(); System.out.println("a: " + (++idx)); ca.await(); } } catch (Exception e) { e.printStackTrace(); } finally { lock.unlock(); } } } private static class B implements Runnable { @Override public void run() { try { lock.lock(); for (int i = 0; i < 10; i++) { cc.signalAll(); System.out.println("b: " + (++idx)); cb.await(); } } catch (Exception e) { e.printStackTrace(); } finally { lock.unlock(); } } } private static class C implements Runnable { @Override public void run() { try { lock.lock(); for (int i = 0; i < 10; i++) { ca.signalAll(); System.out.println("c: " + (++idx)); cc.await(); } } catch (Exception e) { e.printStackTrace(); } finally { lock.unlock(); } } } public static void main(String[] args) { new Thread(new A()).start(); new Thread(new B()).start(); new Thread(new C()).start(); }
上述示例定義了 3 個線程類 A、B 和 C,并按照 A -> B -> C
的順序進行組織,各個線程在調用 Lock#lock
方法獲取到鎖之后會先嘗試通知后繼線程(將對應的線程移入到同步隊列),然后對 idx 變量進行累加并打印,接著進入等待狀態并釋放資源,方法 Lock#unlock
接下來會調度位于同步隊列隊頭結點的線程繼續執行。
ReentrantLock 實現了 Lock 接口,該接口抽象了鎖應該具備的基本操作,包括鎖資源的獲取、釋放,以及創建條件對象。除了本文介紹的 ReentrantLock 外,JUC 中直接或間接實現了 Lock 接口的組件還包括 ReentrantReadWriteLock 和 StampedLock,我們將在后面的文章中對這些組件逐一分析。Lock 接口的定義如下:
public interface Lock { void lock(); void lockInterruptibly() throws InterruptedException; boolean tryLock(); boolean tryLock(long time, TimeUnit unit) throws InterruptedException; void unlock(); Condition newCondition(); }
各方法釋義如下:
lock()
:獲取鎖資源,如果獲取失敗則阻塞。
lockInterruptibly()
:獲取鎖資源,如果獲取失敗則阻塞,阻塞期間支持響應中斷請求。
tryLock()
:嘗試獲取鎖資源,不管是否獲取成功都立即返回,如果獲取成功則返回 true,否則返回 false。
tryLock(long time, TimeUnit unit)
:嘗試獲取鎖資源,相對于無參版本的 tryLock 方法引入了超時機制,并支持在等待期間響應中斷請求。
unlock()
:釋放鎖資源。
newCondition()
:創建一個綁定到當前 Lock 上的條件對象。
上一小節分析了 Lock 接口的定義,ReentrantLock 實現了該接口,并將接口方法的實現都委托給了 Sync 內部類處理。Sync 是一個抽象類,繼承自 AbstractQueuedSynchronizer,并派生出 FairSync 和 NonfairSync 兩個子類(繼承關系如下圖),由命名可以看出 FairSync 實現了公平鎖,而 NonfairSync 則實現了非公平鎖。
ReentrantLock 提供了帶 boolean 參數的構造方法,依據該參數來決定是創建公平鎖還是非公平鎖(默認為非公平鎖),構造方法定義如下:
public ReentrantLock() { // 默認創建非公平鎖 sync = new NonfairSync(); } public ReentrantLock(boolean fair) { // 依據參數決定創建公平鎖還是非公平鎖 sync = fair ? new FairSync() : new NonfairSync(); }
下面將區分公平鎖和非公平鎖分析 ReentrantLock 針對 Lock 接口方法的具體實現,在開始之前先介紹一下 AQS 中的 state 字段在 ReentrantLock 中的作用。
我們知道 ReentrantLock 是可重入的,這里的可重入是指當一個線程獲取到 ReentrantLock 鎖之后,如果該線程再次嘗試獲取該 ReentrantLock 鎖時仍然可以獲取成功,對應的重入次數加 1。ReentrantLock 的重入次數則由 AQS 的 state 字段進行記錄。當 state 為 0 時,說明目標 ReentrantLock 鎖當前未被任何線程持有,當一個線程釋放 ReentrantLock 鎖時,對應的 state 值需要減 1。
本小節我們來分析一下非公平鎖 NonfairSync 的實現機制,首先來看一下 NonfairSync#lock
方法,該方法用于獲取資源,如果獲取失敗則會將當前線程加入到同步隊列中阻塞等待。方法實現如下:
final void lock() { // 嘗試獲取鎖,將 state 由 0 設置為 1 if (this.compareAndSetState(0, 1)) { // 首次獲取鎖成功,記錄當前鎖對象 this.setExclusiveOwnerThread(Thread.currentThread()); } else { // 目標鎖對象已經被占用,或者非首次獲取目標鎖對象 this.acquire(1); } }
方法 NonfairSync#lock
加鎖的過程首先會基于 CAS 操作嘗試將 ReentrantLock 的 state 值由 0 改為 1,搶占鎖資源,這也是非公平語義的根本所在。如果操作成功,則說明目標 ReentrantLock 鎖當前未被任何線程持有,且本次加鎖成功。如果操作失敗則區分兩種情況:
目標 ReentrantLock 鎖已被當前線程持有。
目標 ReentrantLock 鎖已被其它線程持有。
針對這兩種情況,接下來會調用 AbstractQueuedSynchronizer#acquire
方法嘗試獲取 1 個單位的資源,該方法由 AQS 實現,我們已經在前面的文章中分析過,其中會執行模板方法 AbstractQueuedSynchronizer#tryAcquire
。NonfairSync 針對該模板方法的實現如下:
protected final boolean tryAcquire(int acquires) { return this.nonfairTryAcquire(acquires); }
上述方法將嘗試獲取資源的邏輯委托給 Sync#nonfairTryAcquire
方法執行,ReentrantLock 的 ReentrantLock#tryLock()
方法同樣基于該方法實現。下面來分析一下該方法的執行邏輯,實現如下:
final boolean nonfairTryAcquire(int acquires) { // 獲取當前線程對象 final Thread current = Thread.currentThread(); // 獲取 state 值 int c = this.getState(); if (c == 0) { // state 為 0,表示目標鎖當前未被持有,嘗試獲取鎖 if (this.compareAndSetState(0, acquires)) { this.setExclusiveOwnerThread(current); return true; } } // 如果當前已經持有鎖的線程已經是當前線程 else if (current == this.getExclusiveOwnerThread()) { // 重入次數加 1 int nextc = c + acquires; if (nextc < 0) { // 重入次數溢出 throw new Error("Maximum lock count exceeded"); } // 更新 state 記錄的重入次數 this.setState(nextc); return true; } // 已經持有鎖的線程不是當前線程,嘗試加鎖失敗 return false; }
方法 Sync#nonfairTryAcquire
的執行流程可以概括為;
獲取當前 ReentrantLock 鎖的 state 值;
如果 state 值為 0,說明當前 ReentrantLock 鎖未被任何線程持有,基于 CAS 嘗試將 state 值由 0 改為 1,搶占鎖資源,修改成功即為加鎖成功;
否則,如果當前已經持有該 ReentrantLock 鎖的線程是自己,則修改重入次數(即將 state 值加 1);
否則,目標 ReentrantLock 鎖已經被其它線程持有,加鎖失敗。
如果 Sync#nonfairTryAcquire
方法返回 false,則說明當前線程嘗試獲取目標 ReentrantLock 鎖失敗,對于 ReentrantLock#lock
方法而言,接下去線程會被加入到同步隊列阻塞等待,而對于 ReentrantLock#tryLock()
方法而言,線程會立即退出,并返回 false。
方法 ReentrantLock#newCondition
同樣是委托給 Sync#newCondition
方法處理,該方法只是簡單的創建了一個 ConditionObject 對象,即新建了一個條件隊列。非公平鎖 NonfairSync 中的以下方法都是直接委托給 AQS 處理,這些方法的實現機制已在前面分析 AQS 時介紹過:
ReentrantLock#lockInterruptibly
:直接委托給 AbstractQueuedSynchronizer#acquireInterruptibly
方法實現,獲取的資源數為 1。
ReentrantLock#tryLock(long, java.util.concurrent.TimeUnit)
:直接委托給 AbstractQueuedSynchronizer#tryAcquireNanos
方法實現,獲取的資源數為 1。
ReentrantLock#unlock
:直接委托給 AbstractQueuedSynchronizer#release
方法實現,釋放的資源數為 1。
前面的文章,我們在分析 AQS 的 AbstractQueuedSynchronizer#release
方法時,曾介紹過該方法會調用模板方法 AbstractQueuedSynchronizer#tryRelease
以嘗試釋放資源。ReentrantLock 針對該模板方法的實現位于 Sync 抽象類中,所以它是一個由 NonfairSync 和 FairSync 共用的方法,下面來分析一下該方法的實現。
protected final boolean tryRelease(int releases) { // 將當前 state 記錄的重入次數減 1 int c = this.getState() - releases; // 如果當前持有鎖的線程對象不是當前線程則拋出異常 if (Thread.currentThread() != this.getExclusiveOwnerThread()) { throw new IllegalMonitorStateException(); } boolean free = false; // 如果重入次數已經降為 0,則清空持有當前鎖的線程對象 if (c == 0) { free = true; this.setExclusiveOwnerThread(null); } // 更新當前鎖的重入次數 this.setState(c); return free; }
嘗試釋放資源的過程本質上就是修改 state 字段值的過程,如果當前操作的線程是持有 ReentrantLock 鎖的線程,則上述方法會將 state 值減 1,即將已重入次數減 1。如果修改后的 state 字段值為 0,則說明當前線程已經釋放了持有的 ReentrantLock 鎖,此時需要清除記錄在 ReentrantLock 對象中的線程 Thread 對象。
本小節我們來分析一下公平鎖 FairSync 的實現機制,這里的公平本質上是指公平的獲取鎖資源,所以主要的區別體現在加鎖的過程,即 ReentrantLock#lock
方法。
前面我們在分析 NonfairSync 時看到,NonfairSync 在加鎖時首先會基于 CAS 嘗試將 state 值由 0 改為 1,失敗的情況下才會繼續調用 AbstractQueuedSynchronizer#acquire
方法等待獲取資源,并且在同步隊列中等待期間仍然會在 state 為 0 時搶占獲取鎖資源。
FairSync 相對于 NonfairSync 的區別在于當 state 值為 0 時,即目標 ReentrantLock 鎖此時未被任何線程持有的情況下,FairSync 并不會去搶占鎖資源,而是檢查同步隊列中是否有排在前面等待獲取鎖資源的其它線程,如果有則讓渡這些排在前面的線程優先獲取鎖資源。
下面來看一下 FairSync#lock
方法的實現,該方法只是簡單的將獲取鎖資源操作委托給 AQS 的 AbstractQueuedSynchronizer#acquire
方法執行,所以我們需要重點關注一下模板方法 FairSync#tryAcquire
的實現:
protected final boolean tryAcquire(int acquires) { // 獲取當前線程對象 final Thread current = Thread.currentThread(); // 獲取當前 state 值 int c = this.getState(); if (c == 0) { // state 為 0,表示目標鎖當前未被持有,先檢查是否有阻塞等待當前鎖的線程,如果沒有再嘗試獲取鎖 if (!this.hasQueuedPredecessors() && this.compareAndSetState(0, acquires)) { this.setExclusiveOwnerThread(current); return true; } } // 如果當前已經持有鎖的線程已經是當前線程,則修改已重入次數加 1 else if (current == this.getExclusiveOwnerThread()) { int nextc = c + acquires; if (nextc < 0) { throw new Error("Maximum lock count exceeded"); } this.setState(nextc); return true; } return false; } }
上述方法的執行流程與 NonfairSync 中的相關實現大同小異,主要區別在于當 state 值為 0 時,FairSync 會調用 AbstractQueuedSynchronizer#hasQueuedPredecessors
檢查當前同步隊列中是否還有等待獲取鎖資源的其它線程,如果存在則優先讓這些線程獲取鎖資源,并將自己加入到同步隊列中排隊等待。
感謝各位的閱讀,以上就是“JUC的ReentrantLock怎么使用”的內容了,經過本文的學習后,相信大家對JUC的ReentrantLock怎么使用這一問題有了更深刻的體會,具體使用情況還需要大家實踐驗證。這里是億速云,小編將為大家推送更多相關知識點的文章,歡迎關注!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。