五萬字的Java同步器框架AbstractQueuedSynchronizer(AQS)源碼的深度解析與應用。包括鎖的獲取與釋放、同步隊列、條件隊列的原理,並提供了大量的自定義鎖的實現和生產消費模型的案例!
文章目錄
- 1 從AQS學起
- 2 AQS的設計
- 3 Lock接口
- 4 不可重入獨佔鎖簡單實現
- 5 同步隊列
- 5.1 同步隊列的結構
- 5.2 鎖的獲取與釋放
- 5.3 acquire獨佔式獲取鎖
- 5.3.1 tryAcquire嘗試獲取獨佔鎖
- 5.3.2 addWaiter加入到同步隊列
- 5.3.3 acquireQueued結點自旋獲取鎖
- 5.3.3.1 shouldParkAfterFailedAcquire結點是否應該掛起
- 5.3.3.2 parkAndCheckInterrupt掛起線程&判斷中斷狀態
- 5.3.3.3 finally代碼塊
- 5.3.4 selfInterrupt安全中斷
- 5.4 release獨佔式鎖釋放
- 5.5 acquirelnterruptibly獨佔式可中斷獲取鎖
- 5.6 tryAcquireNanos獨佔式超時獲取鎖
- 5.7 獨佔式獲取/釋放鎖總結
- 5.8 acquireShared共享式獲取鎖
- 5.9 reaseShared共享式釋放鎖
- 5.10 acquireSharedInterruptibly共享式可中斷獲取鎖
- 5.11 tryAcquireSharedNanos共享式超時獲取鎖
- 5.12 共享式獲取/釋放鎖總結
- 6 鎖的簡單實現
- 7 條件隊列
- 7.1 Condition概述
- 7.2 條件隊列的結構
- 7.3 等待機制原理
- 7.3.1 await()響應中斷等待
- 7.3.1.1 addConditionWaiter添加結點到條件隊列
- 7.3.1.2 unlinkCancelledWaiters清除取消等待的結點
- 7.3.1.3 fullyRelease釋放所有重入鎖
- 7.3.1.4 isOnSyncQueue結點是否在同步隊列中
- 7.3.1.5 checkInterruptWhileWaiting檢測中斷以及被喚醒原因
- 7.3.1.6 reportInterruptAfterWait對中斷模式進行處理
- 7.3.2 await(time, TimeUnit)超時等待一段時間
- 7.3.3 awaitUntil(deadline)超時等待時間點
- 7.3.4 awaitNanos(nanosTimeout) 超時等待納秒
- 7.3.5 awaitUninterruptibly()不響應中斷等待
- 7.4 通知機制原理
- 7.5 Condition的應用
- 8 總結
1 從AQS學起
public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer
implements Serializable
AbstractQueuedSynchronizer,來自於JDK1.5,位於JUC包,由併發編程大師Doug Lea編寫,字面翻譯就是“抽象隊列同步器”,簡稱爲AQS。AQS作爲一個抽象類,是構建JUC包中的鎖(比如ReentrantLock)或者其他同步組件(比如CountDownLatch)的底層基礎框架。
在每一個同步組件的實現類中,都具有AQS的實現類作爲內部類,被用來實現該鎖的內存語義,並且他們之間是強關聯關係,從對象的關係上來說鎖或同步器與AQS是:“聚合關係”。
也可以這樣理解二者之間的關係:鎖(比如ReentrantLock)是面向使用者(大部分“用輪子”程序員)的,它定義了使用者與鎖交互的外部接口,比如獲得鎖、釋放鎖的接口,這樣就隱藏了實現細節,方便學習使用;而AQS則是面向的是鎖的實現者(少部分“造輪子”的程序員,比如Doug Lea,這個比喻並不恰當,因爲Doug Lea是整個JUC包的編寫者,包括AQS也是他寫的),因爲AQS簡化了鎖的實現方式,屏蔽了同步狀態管理、線程的排隊、線程的等待與喚醒等更加底層的操作,我們如果想要自己寫一個“鎖”—造輪子,那麼就可以直接利用AQS框架實現,而不需要去關心上面那些更加底層的東西。這樣的話似乎也不算真正的從零開始造輪子,因爲我們用的AQS這個製造工具也是別人(Doug Lea)製作的::>_<::。
鎖和AQS很好地隔離了使用者和實現者所需關注的領域。如果我們只是想單純的使用某個鎖,那個直接看鎖的API就行了,而如果我們想要看懂某個鎖的實現,那麼我們就需要看鎖的源碼,在這之中我們又可能會遇到AQS框架的某個方法的調用;如果我們想要走得更遠,那麼此時又會進入AQS的源碼,那麼我們必須去了解AQS這個同步框架的設計與實現!
如果我們想要真正學搞懂JUC的locks部分,那麼,先從AQS學起吧!
2 AQS的設計
AbstractQueuedSynchronizer被設計爲一個抽象類,它使用了一個volatile int類型的成員變量state來表示同步狀態,通過內置的FIFO雙向隊列來完成資源獲取線程的排隊等待工作。通常AQS的子類通過繼承AQS並實現它的抽象方法來管理同步狀態。
AQS的子類常常作爲同步組件的靜態內部類(也就是聚合關係),AQS自身沒有實現任何同步接口,它僅僅是定義了若干同步狀態獲取和釋放的方法來供同步組件使用,AQS既可以支持獨佔式地訪問同步狀態(比如ReentrantLock),也可以支持共享式地訪問同步狀態(比如CountDownLatch),這樣就可以方便實現不同類型的同步組件。
AQS的方法設計是基於模板方法模式的,也就是說實現者需要繼承AQS並按照需要重寫指定的方法,隨後將AQS的實現類組合在同步組件的實現中,並最終調用AQS提供的模板方法來實現同步,而這些模板方法內部實際上被設計成會調用使用者重寫的方法。
因此,AQS的方法可以分爲三大類:固定方法、可重寫的方法、模版方法。
2.1 固定方法
重寫AQS指定的方法時,需要使用AQS提供的如下3個方法來訪問或修改同步狀態,不同的鎖實現都可以直接調用這三個方法:
- getState():獲取當前最新同步狀態。
- setState(int newState):設置當前最新同步狀態。
- compareAndSetState(int expect,int update):使用CAS設置當前狀態,該方法能夠保證狀態設置的原子性。
它們的源碼很簡單:
/**
* int類型同步狀態變量,或者代表共享資源,被volatile修飾,具有volatile的讀、寫的內存語義
*/
private volatile int state;
/**
* @return 返回同步狀態的當前值。此操作具有volatile讀的內存語義,因此每次獲取的都是最新值
*/
protected final int getState() {
return state;
}
/**
* @param newState 設置同步狀態的最新值。此操作具有volatile寫的內存語義,因此每次寫數據都是寫回主存並導致其它緩存實效
*/
protected final void setState(int newState) {
state = newState;
}
/**
* 如果當前狀態值等於預期值,則以原子方式將同步狀態設置爲給定的更新值。
* 此操作具有volatile讀取和寫入的內存語義。
*
* @param expect 預期值
* @param update 寫入值
* @return 如果更新成功返回true,失敗則返回false
*/
protected final boolean compareAndSetState(int expect, int update) {
//內部調用unsafe的方法,該方法是一個CAS方法
//這個unsafe類,實際上是比AQS更加底層的底層框架,或者可以認爲是AQS框架的基石。
//CAS操作在Java中的最底層的實現就是Unsafe類提供的,它是作爲Java語言與Hospot源碼(C++)以及底層操作系統溝通的橋樑
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
這三個方法getState()、setState()、compareAndSetState()都是final方法,是AQS提供的通用設置同步狀態的方法,能保證線程安全,我們直接調用即可。
2.2 可重寫的方法
可重寫的方法在AQS中一般都沒有提供實現,並且如果子類不重寫直接調用還會拋出異常,這些方法一般是對同步狀態的單次嘗試獲取、釋放(即加鎖、解鎖),並沒有後續失敗處理的方法!實現者一般根據需要重寫對應的方法!
AQS可重寫的方法如下所示:
/**
* 獨佔式獲取鎖,該方法需要查詢當前狀態並判斷鎖是否符合預期,然後再進行CAS設置鎖。返回true則成功,否則失敗。
*
* @param arg 參數,在實現的時候可以傳遞自己想要的數據
* @return 返回true則成功,否則失敗。
*/
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
/**
* 獨佔式釋放鎖,等待獲取鎖的線程將有機會獲取鎖。返回true則成功,否則失敗。
*
* @param arg 參數,在實現的時候可以傳遞自己想要的數據
* @return 返回true則成功,否則失敗。
*/
protected boolean tryRelease(int arg) {
throw new UnsupportedOperationException();
}
/**
* 共享式獲取鎖,返回大於等於0的值表示獲取成功,否則失敗。
*
* @param arg 參數,在實現的時候可以傳遞自己想要的數據
* @return 返回大於等於0的值表示獲取成功,否則失敗。
* 如果返回值小於0,表示當前線程共享鎖失敗
* 如果返回值大於0,表示當前線程共享鎖成功,並且接下來其他線程嘗試獲取共享鎖的行爲很可能成功
* 如果返回值等於0,表示當前線程共享鎖成功,但是接下來其他線程嘗試獲取共享鎖的行爲會失敗(實際上也有可能成功,在後面的源碼部分會將)
*/
protected int tryAcquireShared(int arg) {
throw new UnsupportedOperationException();
}
/**
* 共享式釋放鎖。返回true成功,否則失敗。
*
* @param arg 參數,在實現的時候可以傳遞自己想要的數據
* @return 返回true成功,否則失敗。
*/
protected boolean tryReleaseShared(int arg) {
throw new UnsupportedOperationException();
}
/**
* 當前AQS是否在獨佔模式下被線程佔用,一般表示是否被前當線程獨佔;如果同步是以獨佔方式進行的,則返回true;其他情況則返回 false
*
* @return 如果同步是以獨佔方式進行的,則返回true;其他情況則返回 false
*/
protected boolean isHeldExclusively() {
throw new UnsupportedOperationException();
}
2.3 模版方法
在實現同步組件的時候,按照需要重寫可重寫的方法,但是直接調用的還是AQS提供的模板方法,模版方法再被Lock接口的方法包裝。
這些模板方法同樣是final的。可以猜測出這些模版方法包含了對上面的可重寫方法的後續處理(比如失敗處理)!AQS的模板方法基本上分爲3類:
- 獨佔式獲取與釋放同步狀態
- 共享式獲取與釋放同步狀態
- 查詢同步隊列中的等待線程情況
獨佔方式:
acquire(int arg): 獨佔式獲取同步狀態,如果當前線程獲取同步狀態成功,則由該方法返回,否則,將會進入同步隊列等待,該方法將會調用重寫的tryAcquire(int arg) 方法。該方法不會響應中斷。
acquireInterruptibly(int arg): 與acquire(int arg) 相同,但是該方法響應中斷,當前線程未獲取到同步狀態而進入同步隊列中,如果當前被中斷,則該方法會拋出InterruptedException並返回。
tryAcquireNanos(int arg,long nanos): 在acquireInterruptibly基礎上增加了超時限制,如果當前線程在超時時間內沒有獲取到同步狀態,那麼將會返回false,獲取到了返回true。
release(int arg) : 獨佔式的釋放同步狀態,該方法會在釋放同步狀態之後,將同步隊列中第一個結點包含的線程喚醒。
共享方式:
acquireShared(int arg): 共享式獲取同步狀態,如果當前線程未獲取到同步狀態,將會進入同步隊列等待。與獨佔式的不同是同一時刻可以有多個線程獲取到同步狀態。該方法不會響應中斷。
acquireSharedInterruptibly(int arg) : 與acquireShared(int arg) 相同,但是該方法響應中斷,當前線程未獲取到同步狀態而進入同步隊列中,如果當前被中斷,則該方法會拋出InterruptedException並返回。
tryAcquireSharedNanos(int arg,long nanos): 在acquireSharedInterruptibly基礎上增加了超時限制,如果當前線程在超時時間內沒有獲取到同步狀態,那麼將會返回false,獲取到了返回true
releaseShared(int arg): 共享式釋放同步狀態
獲取線程等待情況:
getQueuedThreads() : 獲取等待在同步隊列上的線程集合。
2.4 總體結構
AQS中文名爲隊列同步器,可以猜測,它的內部具有一個隊列,實際上也確實如此。
AQS內部使用一個一個FIFO的雙端隊列,被稱爲同步隊列,來完成同步狀態的管理,當前線程獲取同步狀態失敗(獲取鎖失敗)的時候,AQS會將當前線程及其等待狀態信息構造成一個結點Node並將其加入同步隊列中,同時阻塞當前線程,當同步狀態由持有線程釋放的時候,會將同步隊列中的首結點中的線程喚醒使其再次嘗試獲取同步狀態。
同步隊列中的結點Node是AQS中的一個內部類,用來保存獲取同步狀態失敗的線程的線程引用、等待狀態以及前驅結點和後繼結點。AQS外部持有同步隊列的兩個引用,一個指向頭結點head,而另一個指向尾結點tail。
在AQS中還維持了一個volatile int類型的字段state,用來描述同步狀態,可以通過getState、setState、compareAndSetState函數修改其值。
對於不同的鎖實現,state可以有不同的含義。對於ReentrantLock 的實現來說,state可以用來表示當前線程獲取鎖的可重入次數;對於讀寫鎖ReentrantReadWriteLock 來說,state 的高16位表示讀狀態,也就是獲取該讀鎖的次數,低16位表示獲取到寫鎖的線程的可重入次數;對於Semaphore來說,state用來表示當前可用信號的個數:對於CountDownlatch 來說,state 用來表示計數器當前的值。
AQS內部還有一個ConditionObject內部類,用來結合鎖實現更加靈活的線程同步。ConditionObject 可以直接訪問AQS 對象內部的變量,比如state 狀態值和AQS 同步隊列。ConditionObject 又被稱爲條件變量,每個條件變量實例又對應一個條件隊列(單向鏈表,又稱等待隊列),其用來存放調用Condition的await方法後被阻塞的線程,這個等待隊列的頭、尾元素分別由firstWaiter 和lastWaiter持有。
上面的介紹能看出來,AQS中包含兩個隊列的實現,一個同步隊列,用於存放獲取不到鎖的線程,另一個是條件隊列,用於存放調用了await方法的線程,但是兩個隊列中的線程都是WAITING狀態,因爲Lock所底層都是調用的LockSupport.park方法。後面的章節會介紹同步隊列和條件隊列的實現!
3 Lock接口
3.1 Lock接口概述
public interface Lock
Lock接口本來和AQS沒有太多關係的,但是如果想要是實現一個正規的、通用的同步組件(特別是鎖),那就不得不提Lock接口。
Lock接口同樣自於JDK1.5,它被描述成JUC中的鎖的超級接口,所有的JUC中的鎖都會實現Lock接口。
由於它是作爲接口,到這裏或許大家都明白了它的設計意圖,接口就是一種規範。Lcok接口定義了一些抽象方法,用於獲取鎖、釋放鎖等,而所有的鎖都實現Lock接口,那麼它們雖然可能有不同的內部實現,但是開放給外部調用的方法卻是一樣的,這就是一種規範,無論你怎麼實現,你給外界調用的始終是“同一個方法”!因此JUC中的鎖也被常常統稱爲lock鎖。
這種優秀的架構設計,不同的鎖實現統一了鎖獲取、釋放等常規操作的方法,方便外部人員使用和學習!類似的設計在JDBC數據庫驅動上面也能看到!
3.2 Lock接口的API方法
我們來看看實現Lcok接口都需要實現哪些方法!
方法名稱 | 描述 |
lock | 獲取鎖,如果鎖無法獲取,那麼當前的線程被掛起,直到鎖被獲取到,不可被中斷。 |
lockInterruptibly | 獲取鎖,如果獲取到了鎖,那麼立即返回,如果獲取不到,那麼當前線程被掛起,直到當前線程被喚醒或者其他的線程中斷了當前的線程。 |
tryLock | 如果調用的時候能夠獲取鎖,那麼就獲取鎖並且返回true,如果當前的鎖無法獲取到,那麼這個方法會立刻返回false |
tryLcok(long time,TimeUnit unit) | 在指定時間內嘗試獲取鎖。如果獲取了鎖,那麼返回true,如果當前的鎖無法獲取,那麼當前的線程被掛起,直到當前線程獲取到了鎖或者當前線程被其他線程中斷或者指定的等待時間到了。時間到了還沒有獲取到鎖則返回false。 |
unlock | 釋放當前線程佔用的鎖 |
newCondition | 返回一個與當前的鎖關聯的條件變量。在使用這個條件變量之前,當前線程必須佔用鎖。調用Condition的await方法,會在等待之前原子地釋放鎖,並在等待被喚醒後原子的獲取鎖 |
3.3 鎖獲取與中斷
Thread類中有一個interrupt方法,可中斷因爲主動調用Object.wait()、Thread.join()和Thread.sleep()等方法造成的線程等待,以及使用lockInterruptibly方法和tryLock(time,timeUnit)嘗試獲取鎖但是未獲取鎖而造成的阻塞,並且他們都將拋出InterruptedException異常,並且設置該線程的中斷狀態位爲true。對於因爲調用lock()方法,或者因爲無法獲取Synchronized鎖而被阻塞的線程,interrupt方法無法中斷。
下面舉例詳細說明Lock接口的這四種方法的使用:假如線程A和線程B使用同一個鎖LOCK,此時線程A首先獲取到鎖LOCK.lock(),並且始終持有不釋放。如果此時B要去獲取鎖,有四種方式:
- LOCK.lock(): 此方式會使得B始終處於等待中,即使調用B.interrupt()也不能中斷,除非線程A調用LOCK.unlock()釋放鎖。
- LOCK.lockInterruptibly(): 此方式會使得B等待,但當調用B.interrupt()會被中斷等待,並拋出InterruptedException異常,否則會與lock()一樣始終處於等待中,直到線程A釋放鎖。
- LOCK.tryLock(): 調用該方法時B不會等待,一次獲取不到鎖就直接返回false。
- LOCK.tryLock(10, TimeUnit.SECONDS): 該處會在10秒時間內處於等待中,但當調用B.interrupt()會中斷等待,並拋出InterruptedException。 10秒時間內如果線程A釋放鎖,線程B會獲取到鎖並返回true,否則10秒過後會直接返回false,去執行下面的邏輯。
3.4 Synchronized和Lock的區別
- 首先synchronized是java內置關鍵字,是jvm層面實現的;Lock是個java接口,可以代表JUC中的鎖,通過java代碼實現。
- synchronized會自動釋放鎖(a 線程執行完同步代碼會釋放鎖 ;b線程執行過程中發生異常會釋放鎖),Lock需在finally中手工釋放鎖(unlock()方法釋放鎖),否則容易造成線程死鎖;即lock需要顯示的獲得、釋放鎖,synchronized隱式獲得、釋放鎖。
- lock在等待鎖過程中可以用lockInterruptibly()方法配合interrupt方法()來中斷等待,也可以使用tryLock()設置等待超時時間,而synchronized只能等待鎖的釋放,不能響應中斷。
- synchronized是非公平鎖,而Lock鎖可以實現爲公平鎖或者非公平鎖。
- Lock鎖將監視器monitor方法,單獨的封裝到了一個Condition對象當中,更加的面向對象了,而且一個Lock鎖可以擁有多個condition對象(條件隊列),singal和signalAll可以選擇喚醒不同的隊列中的線程;而同一個synchronized塊只能有一個監視器對象,一個條件隊列。
- Lock可以提高多個線程進行讀操作的效率,非常靈活。(可以通過readwritelock實現讀寫分離)。
- synchronized使用Thread.holdLock(監視器對象)檢測當前線程是否持有鎖,Lock可以通過lock.trylock或者isHeldExclusively方法判斷是否獲取到鎖。
4 不可重入獨佔鎖簡單實現
經過上面的學習,我們知道了AQS的大概設計思路與方法,以及規範的鎖需要實現Lock接口,現在我們嘗試自己構建一個簡單的獨佔鎖。
顧名思義,獨佔鎖就是在同一時刻只能有一個線程獲取到鎖,而其他獲取鎖的線程只能
處於同步隊列中等待,只有獲取鎖的線程釋放了鎖,後繼的線程才能夠獲取鎖,下面是一個基於AQS的獨佔鎖的實現。
我們需要重寫tryAcquire和tryRelease(int releases)方法。將同步狀態state值爲1看鎖被獲取了,使用setExclusiveOwnerThread方法記錄獲取到鎖的線程,state爲0看作鎖沒被獲取。另外,下面的簡單實現並沒有可重入的考慮,因此不具備重入性!
從實現中能夠看出來,有了AQS工具,我們實現自定義同步組件還是比較簡單的!
/**
* @author lx
*/
public class ExclusiveLock implements Lock {
/**
* 將AQS的實現組合到鎖的實現內部
* 對於同步狀態state,這裏的實現將1看成同步,0看成未同步
*/
private static class Sync extends AbstractQueuedSynchronizer {
/**
* 重寫isHeldExclusively方法
*
* @return 是否處於鎖佔用狀態
*/
@Override
protected boolean isHeldExclusively() {
//state是否等於1
return getState() == 1;
}
/**
* 重寫tryAcquire方法,嘗試獲取鎖
* 這裏的實現爲:當state狀態爲0的時候可以獲取鎖
*
* @param acquires 參數,這裏我們沒用到
* @return 獲取成功返回true,失敗返回false
*/
@Override
public boolean tryAcquire(int acquires) {
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
/**
* 重寫tryRelease方法,釋放鎖
* 這裏的實現爲:當state狀態爲1的時候,將狀態設置爲0
*
* @param releases 參數,這裏我們沒用到
* @return 釋放成功返回true,失敗返回false
*/
@Override
protected boolean tryRelease(int releases) {
//如果嘗試解鎖的線程不是加鎖的線程,那麼拋出異常
if (Thread.currentThread() != getExclusiveOwnerThread()) {
throw new IllegalMonitorStateException();
}
//設置當前擁有獨佔訪問權限的線程爲null
setExclusiveOwnerThread(null);
setState(0);
return true;
}
/**
* 返回一個Condition,每個condition都包含了一個condition隊列
* 用於實現線程在指定條件隊列上的主動等待和喚醒
*
* @return 每次調用返回一個新的ConditionObject
*/
Condition newCondition() {
return new ConditionObject();
}
}
/**
* 僅需要將操作代理到Sync實例上即可
*/
private final Sync sync = new Sync();
/**
* lock接口的lock方法
*/
@Override
public void lock() {
sync.acquire(1);
}
/**
* lock接口的tryLock方法
*/
@Override
public boolean tryLock() {
return sync.tryAcquire(1);
}
@Override
public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}
@Override
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
/**
* lock接口的unlock方法
*/
@Override
public void unlock() {
sync.release(1);
}
/**
* lock接口的newCondition方法
*/
@Override
public Condition newCondition() {
return sync.newCondition();
}
public boolean isLocked() {
return sync.isHeldExclusively();
}
public boolean hasQueuedThreads() {
return sync.hasQueuedThreads();
}
}
5 同步隊列
5.1 同步隊列的結構
public abstract class AbstractQueuedSynchronizer extends
AbstractOwnableSynchronizer implements java.io.Serializable {
/**
* 當前獲取鎖的線程,該變量定義在父類中,AQS直接繼承。在獨佔鎖的獲取時,如果是重入鎖,那麼需要知道到底是哪個線程獲得了鎖。沒有就是null
*/
private transient Thread exclusiveOwnerThread;
/**
* AQS中保持的對同步隊列的引用
* 隊列頭結點,實際上是一個哨兵結點,不代表任何線程,head所指向的Node的thread屬性永遠是null。
*/
private transient volatile Node head;
/**
* 隊列尾結點,後續的結點都加入到隊列尾部
*/
private transient volatile Node tail;
/**
* 同步狀態
*/
private volatile int state;
/**
* Node內部類,同步隊列的結點類型
*/
static final class Node {
/*AQS支持共享模式和獨佔模式兩種類型,下面表示構造的結點類型標記*/
/**
* 共享模式下構造的結點,用來標記該線程是獲取共享資源時被阻塞掛起後放入AQS 隊列的
*/
static final Node SHARED = new Node();
/**
* 獨佔模式下構造的結點,用來標記該線程是獲取獨佔資源時被阻塞掛起後放入AQS 隊列的
*/
static final Node EXCLUSIVE = null;
/*線程結點的等待狀態,用來表示該線程所處的等待鎖的狀態*/
/**
* 指示當前結點(線程)需要取消等待
* 由於在同步隊列中等待的線程發生等待超時、中斷、異常,即放棄獲取鎖,需要從同步隊列中取消等待,就會變成這個狀態
* 如果結點進入該狀態,那麼不會再變成其他狀態
*/
static final int CANCELLED = 1;
/**
* 指示當前結點(線程)的後續結點(線程)需要取消等待(被喚醒)
* 如果一個結點狀態被設置爲SIGNAL,那麼後繼結點的線程處於掛起或者即將掛起的狀態
* 當前結點的線程如果釋放了鎖或者放棄獲取鎖並且結點狀態爲SIGNAL,那麼將會嘗試喚醒後繼結點的線程以運行
* 這個狀態通常是由後繼結點給前驅結點設置的。一個結點的線程將被掛起時,會嘗試設置前驅結點的狀態爲SIGNAL
*/
static final int SIGNAL = -1;
/**
* 線程在等待隊列裏面等待,waitStatus值表示線程正在等待條件
* 原本結點在等待隊列中,結點線程等待在Condition上,當其他線程對Condition調用了signal()方法之後
* 該結點會從從等待隊列中轉移到同步隊列中,進行同步狀態的獲取
*/
static final int CONDITION = -2;
/**
* 釋放共享資源時需要通知其他結點,waitStatus值表示下一個共享式同步狀態的獲取應該無條件傳播下去
*/
static final int PROPAGATE = -3;
/**
* 記錄當前線程等待狀態值,包括以上4中的狀態,還有0,表示初始化狀態
*/
volatile int waitStatus;
/**
* 前驅結點,當結點加入同步隊列將會被設置前驅結點信息
*/
volatile Node prev;
/**
* 後繼結點
*/
volatile Node next;
/**
* 當前獲取到同步狀態的線程
*/
volatile Thread thread;
/**
* 等待隊列中的後繼結點,如果當前結點是共享模式的,那麼這個字段是一個SHARED常量
* 在獨佔鎖模式下永遠爲null,僅僅起到一個標記作用,沒有實際意義。
*/
Node nextWaiter;
/**
* 如果是共享模式下等待,那麼返回true(因爲上面的Node nextWaiter字段在共享模式下是一個SHARED常量)
*/
final boolean isShared() {
return nextWaiter == SHARED;
}
/**
* 用於建立初始頭結點或SHARED標記
*/
Node() {
}
/**
* 用於添加到等待隊列
*
* @param thread
* @param mode
*/
Node(Thread thread, Node mode) {
this.nextWaiter = mode;
this.thread = thread;
}
//......
}
}
}
由上面的源碼可知,同步隊列的基本結構如下圖:
在AQS內部Node的源碼中我們能看到,同步隊列是"CLH" (Craig, Landin, andHagersten) 鎖隊列的變體,它的head引用指向的頭結點作爲哨兵結點,不存儲任何與等待線程相關的信息,或者可以看成已經獲得鎖的結點。第二個結點開始纔是真正的等待線程構建的結點,後續的結點會加入到鏈表尾部。
將新結點添加到鏈表尾部的方法是compareAndSetTail(Node expect,Node update)方法,該方法是一個CAS方法,能夠保證線程安全。
最終獲取鎖的線程所在的結點,會被設置成爲頭結點(setHead方法),該設置步驟是通過獲取鎖成功的線程來完成的,由於只有一個線程能夠成功獲取到鎖,因此設置的方法並不需要使用CAS來保證。
同步隊列遵循先進先出(FIFO),頭結點的next結點是將要獲取到鎖的結點,線程在釋放鎖的時候將會喚醒後繼結點,然後後繼結點會嘗試獲取鎖。
5.2 鎖的獲取與釋放
Lock中“鎖”的狀態使用state變量來表示,一般來說0表示鎖沒被佔用,大於0表示所已經被佔用了。
AQS提供的鎖的獲取和釋放分爲獨佔式的和共享式的:
- 獨佔式:顧名思義就是同一時刻只能有一個線程獲取到鎖,其他獲取鎖線程只能處於同步隊列中等待,只有獲取鎖的線程釋放了鎖,後繼的線程才能夠獲取到鎖。
- 共享式:同一時刻能夠有多個線程獲取到鎖。
對於AQS 來說,線程同步的關鍵是對同步狀態state的操作:
- 在獨佔式下獲取和釋放鎖使用的方法爲: void acquire( int arg) 、void acquirelnterruptibly(int arg) 、boolean release( int arg)。
- 在共享式下獲取和釋放鎖的方法爲: void acquireShared(int arg) 、void acquireSharedInterruptibly(int arg)、 boolean reaseShared(int arg)。
獲取鎖的大概通用流程如下:
線程會首先嚐試獲取鎖,如果失敗,則將當前線程以及等待狀態等信息包成一個Node結點加到同步隊列裏。接着會不斷循環嘗試獲取鎖(獲取鎖的條件是當前結點爲head的直接後繼纔會嘗試),如果失敗則會嘗試阻塞自己(阻塞的條件是當前節結點的前驅結點是SIGNAL狀態),阻塞後將不會執行後續代碼,直至被喚醒;當持有鎖的線程釋放鎖時,會喚醒隊列中的後繼線程,或者阻塞的線程被中斷或者時間到了,那麼阻塞的線程也會被喚醒。
如果分獨佔式和共享式,那麼在上面的通用步驟之下有這些區別:
- 獨佔式獲取的鎖是與具體線程綁定的,就是說如果一個線程獲取到了鎖,exclusiveOwnerThread字段就會記錄這個線程,其他線程再嘗試操作state 獲取鎖時會發現當前該鎖不是自己持有的,就會在獲取失敗後被放入AQS 同步隊列。比如獨佔鎖ReentrantLock 的實現, 當一個線程獲取了ReentrantLock 的鎖後,在AQS 內部會首先使用CAS操作把state 狀態值從0變爲1 ,然後設置當前鎖的持有者爲當前線程,當該線程再次獲取鎖時發現它就是鎖的持有者,則會把狀態值從1變爲2,也就是設置可重入次數,而當另外一個線程獲取鎖時發現自己並不是該鎖的持有者就會被放入AQS 同步隊列後掛起。
- 共享式獲取的鎖與具體線程是不相關的,當多個線程去請求鎖時通過CAS 方式競爭獲取鎖,當一個線程獲取到了鎖後,另外一個線程再次去獲取時如果當前鎖還能滿足它的需要,則當前線程只需要使用CAS 方式進行獲取即可。比如Semaphore 信號量, 當一個線程通過acquire()方法獲取信號量時,會首先看當前信號量個數是否滿足需要,不滿足則把當前線程放入同步隊列,如果滿足則通過自旋CAS 獲取信號量,相應的信號量個數減少對應的值。
實際上,具體的步驟更加複雜,下面講解源碼的時候會提到!
5.3 acquire獨佔式獲取鎖
通過調用AQS的acquire模版方法可以獨佔式的獲取鎖,該方法不會響應中斷,也就是由於線程獲取同步狀態失敗後進入同步隊列中,後續對線程進行中斷操作時,線程不會從同步隊列中移出。基於獨佔式實現的組件有ReentrantLock等。
該方法大概步驟如下:
- 首先調用tryAcquire方法嘗試獲取鎖,如果獲取鎖成功會返回true,方法結束;否則獲取鎖失敗返回false,然後進行下一步的操作。
- 通過addWaiter方法將線程按照獨佔模式Node.EXCLUSIVE構造同步結點,並添加到同步隊列的尾部。
- 然後通過acquireQueued(Node node,int arg)方法繼續自旋獲取鎖。
- 一次自旋中如果獲取不到鎖,那麼判斷是否可以掛起並嘗試掛起結點中的線程(調用LockSupport.park(this)方法掛起自己,注意這裏的線程狀態是WAITING)。而掛起線程的喚醒主要依靠前驅結點或線程被中斷來實現,注意喚醒之後會繼續自旋嘗試獲得鎖。
- 最終只有獲得鎖的線程才能從acquireQueued方法返回,然後根據返回值判斷是否調用selfInterrupt設置中斷標誌位,但此時線程處於運行態,即使設置中斷標誌位也不會拋出異常(即acquire(lock)方法不會響應中斷)。
- 線程獲得鎖,acquire方法結束,從lock方法中返回,繼續後續執行同步代碼!
/**
* 獨佔式的嘗試獲取鎖,一直獲取不成功就進入同步隊列等待
*/
public final void acquire(int arg) {
//內部是由4個方法的調用組成的
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
5.3.1 tryAcquire嘗試獲取獨佔鎖
熟悉的tryAcquire方法,這個方法我們在最開頭講“AQS的設計”時就提到過,該方法是AQS的子類即我們自己實現的,用於首次嘗試獲取獨佔鎖,一般來說就是對state的改變、或者重入鎖的檢查、設置當前獲得鎖的線程等等,不同的鎖有自己相應的邏輯判斷,這裏不多講,後面講具體鎖的實現的時候(比如ReentrantLock)會講到。總之,獲取成功該方法就返回true,失敗就返回false。
在AQS的中tryAcquire的實現爲拋出異常,因此需要子類重寫:
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
5.3.2 addWaiter加入到同步隊列
addWaiter方法是AQS提供的,也不需要我們重寫,或者說是鎖的通用方法!
addWaiter方法用於將按照獨佔模式構造的同步結點Node.EXCLUSIVE添加到同步隊列的尾部。大概步驟爲:
- 按照給定模式,構建新結點。
- 如果同步隊列不爲null,則嘗試將新結點添加到隊列尾部(只嘗試一次),如果添加成功則返回新結點,方法結束。
- 如果隊列爲null或者添加失敗,則調用enq方法循環嘗試添加,直到成功,返回新結點,方法結束。
/**
* addWaiter(Node node)方法將獲取鎖失敗的線程構造成結點加入到同步隊列的尾部
*
* @param mode 模式。獨佔模式傳入的是一個Node.EXCLUSIVE,即null;共享模式傳入的是一個Node.SHARED,即一個靜態結點對象(共享的、同一個)
* @return 返回構造的結點
*/
private Node addWaiter(Node mode) {
/*1 首先構造結點*/
Node node = new Node(Thread.currentThread(), mode);
/*2 嘗試將結點直接放在隊尾*/
//直接獲取同步器的tail結點,使用pred來保存
Node pred = tail;
/*如果pred不爲null,實際上就是隊列不爲null
* 那麼使用CAS方式將當前結點設爲尾結點
* */
if (pred != null) {
node.prev = pred;
//通過使用compareAndSetTail的CAS方法來確保結點能夠被線程安全的添加,雖然不一定能成功。
if (compareAndSetTail(pred, node)) {
//將新構造的結點置爲原隊尾結點的後繼
pred.next = node;
//返回新結點
return node;
}
}
/*
* 3 走到這裏,可能是:
* (1) 由於可能是併發條件,並且上面的CAS操作並沒有循環嘗試,因此可能添加失敗
* (2) 隊列可能爲null
* 調用enq方法,採用自旋方式保證構造的新結點成功添加到同步隊列中
* */
enq(node);
return node;
}
/**
* addWaiter方法中使用到的Node構造器
*
* @param thread 當前線程
* @param mode 模式
*/
Node(Thread thread, AbstractQueuedSynchronizer.Node mode) {
//等待隊列中的後繼結點 就等於該結點的模式
//由此可知,共享模式該值爲Node.SHARED結點常量,獨佔模式該值爲null
this.nextWaiter = mode;
//當前線程
this.thread = thread;
}
5.3.2.1 enq保證結點入隊
enq方法用在同步隊列爲null或者一次CAS添加失敗的時候,enq要保證結點最終必定添加成功。大概步驟爲:
- 開啓一個死循環,在死循環中進行如下操作;
- 如果隊列爲空,那麼初始化隊列,添加一個哨兵結點,結束本次循環,繼續下一次循環;
- 如果隊列不爲空,那麼向前面的方法一樣,則嘗試將新結點添加到隊列尾部,如果添加成功則返回新結點的前驅,循環結束;如果不成功,結束本次循環,繼續下一次循環。
enq方法返回的是新結點的前驅,當然在addWaiter方法中沒有用到。
另外,添加頭結點使用的compareAndSetHead方法和添加尾結點使用的compareAndSetTail方法都是CAS方法,並且都是調用Unsafe類中的本地方法,因爲線程掛機、恢復、CAS操作等最終會通過操作系統中實現,Unsafe類就提供了Java與底層操作系統進行交互的直接接口,這個類的裏面的許多操作類似於C的指針操作,通過找到對某個屬性的偏移量,直接對該屬性賦值,因爲與Java本地方法對接都是Hospot源碼中的方法,而這些的方法都是採用C++寫的,必須使用指針!
也可以說Unsafe是AQS的實現併發控制機制基石。因此在學習AQS的時候,可以先了解Unsafe:Java中的Unsafe類的原理詳解與使用案例。
/**
* 循環,直到尾結點添加成功
*/
private Node enq(final Node node) {
/*死循環操作,直到添加成功*/
for (; ; ) {
//獲取尾結點t
Node t = tail;
/*如果隊列爲null,則初始化同步隊列*/
if (t == null) {
/*調用compareAndSetHead方法,初始化同步隊列
* 注意:這裏是新建了一個空白結點,這就是傳說中的哨兵結點
* CAS成功之後,head將指向該哨兵結點,返回true
* */
if (compareAndSetHead(new Node()))
//尾結點指向頭結點(哨兵結點)
tail = head;
/*之後並沒有結束,而是繼續循環,此時隊列已經不爲空了,因此會進行下面的邏輯*/
}
/*如果隊列不爲null,則和外面的的方法類似,調用compareAndSetTail方法,新建新結點到同步隊列尾部*/
else {
/*1 首先修改新結點前驅的指向,這一步不是安全的
但是沒關係,因爲這一步如果發生了衝突,那麼下面的CAS操作必然之後有一條線程會成功
其他線程將會重新循環嘗試*/
node.prev = t;
/*
* 2 調用compareAndSetTail方法通過CAS方式嘗試將結點添加到同步隊列尾部
* 如果添加成功,那麼才能繼續下一步,結束這個死循環,否則就會不斷循環嘗試添加
* */
if (compareAndSetTail(t, node)) {
//3 修改原尾結點後繼結點的指向
t.next = node;
//返回新結點,結束死循環
return t;
}
}
}
}
/**
* CAS添加頭結點. 僅僅在enq方法中用到
*
* @param update 頭結點
* @return true 成功;false 失敗
*/
private final boolean compareAndSetHead(Node update) {
return unsafe.compareAndSwapObject(this, headOffset, null, update);
}
/**
* CAS添加尾結點. 僅僅在enq方法中用到
*
* @param expect 預期原尾結點
* @param update 新尾結點
* @return true 成功;false 失敗
*/
private final boolean compareAndSetTail(Node expect, Node update) {
return unsafe.compareAndSwapObject(this, tailOffset, expect, update);
}
在addWaiter和enq方法中,成爲尾結點需要三步:
- 設置前驅prev
- 設置tail
- 設置後繼next
由於第二步設置tail是CAS操作,那麼只能保證node的前驅prev一定是正確的,但是此後設置後繼的操作卻不一定能夠馬上成功就切換到了其他線程,此時next可能爲null,但實際他的後繼並不一定真的爲null。
因此同步隊列只能保證前驅prev一定是可靠的,但是next卻不一定可靠,所以後面的源碼的遍歷操作基本上都是從後向前通過前驅prev進行遍歷的。
5.3.3 acquireQueued結點自旋獲取鎖
能夠走到該方法,那麼說明通過了tryAcquire()和addWaiter()方法,表示該線程獲取鎖已經失敗並且被放入同步隊列尾部了。
acquireQueued方法表示結點進入同步隊列之後的動作,實際上就進入了一個自旋的過程,自旋過程中,當條件滿足,獲取到了鎖,就可以從這個自旋中退出並返回,否則可能會阻塞該結點的線程,後續即使阻塞被喚醒,還是會自旋嘗試獲取鎖,直到成功或者而拋出異常。
最終如果該方法會因爲獲取到鎖而退出,則會返回否被中斷標誌的標誌位 或者 因爲異常而退出,則會拋出異常!大概步驟爲:
- 同樣開啓一個死循環,在死循環中進行下面的操作;
- 如果當前結點的前驅是head結點,那麼嘗試獲取鎖,如果獲取鎖成功,那麼當前結點設置爲頭結點head,當前結點線程出隊,表示當前線程已經獲取到了鎖,然後返回是否被中斷標誌,結束循環,進入finally;
- 如果當前結點的前驅不是head結點或者嘗試獲取鎖失敗,那麼判斷當前線程是否應該被掛起,如果返回true,那麼調用parkAndCheckInterrupt掛起當前結點的線程(LockSupport.park 方法掛起線程,線程出於WAITING),此時不再執行後續的步驟、代碼。
- 如果當前線程不應該被掛起,即返回false,那本次循環結束,繼續下一次循環。
- 如果線程被其他線程喚醒,那麼判斷是否是因爲中斷而被喚醒並修改標誌位,同時繼續循環,直到在步驟2獲得鎖,才能跳出循環!(這也是acquire方法不會響應中斷的原理—park方法被中斷時不會拋出異常,僅僅是從掛起狀態返回,然後需要繼續嘗試獲取鎖)
- 最終,線程獲得了鎖跳出循環,或者發生異常跳出循環,那麼會執行finally語句塊,finally中判斷線程是否是因爲發生異常而跳出循環,如果是,那麼執行cancelAcquire方法取消該結點獲取鎖的請求;如果不是,即因爲獲得鎖跳出循環,則finally中什麼也不幹!
/**
* @param node 新結點
* @param arg 參數
* @return 如果在等待時中斷,則返回true
*/
final boolean acquireQueued(final Node node, int arg) {
//failed表示獲取鎖是否失敗標誌
boolean failed = true;
try {
//interrupted表示是否被中斷標誌
boolean interrupted = false;
/*死循環*/
for (; ; ) {
//獲取新結點的前驅結點
final Node p = node.predecessor();
/*只有前驅結點是頭結點的時候才能嘗試獲取鎖
* 同樣調用tryAcquire方法獲取鎖
* */
if (p == head && tryAcquire(arg)) {
//獲取到鎖之後,就將自己設置爲頭結點(哨兵結點),線程出隊列
setHead(node);
//前驅結點(原哨兵結點)的鏈接置空,由JVM回收
p.next = null;
//獲取鎖是否失敗改成false,表示成功獲取到了鎖
failed = false;
//返回interrupted,即返回線程是否被中斷
return interrupted;
}
/*前驅結點不是頭結點或者獲取同步狀態失敗*/
/*shouldParkAfterFailedAcquire檢測線程是否應該被掛起,如果返回true
* 則調用parkAndCheckInterrupt用於將線程掛起
* 否則重新開始循環
* */
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
/*到這一步,說明是當前結點(線程)因爲被中斷而喚醒,那就改變自己的中斷標誌位狀態信息爲true
* 然後又從新開始循環,直到獲取到鎖,才能返回
* */
interrupted = true;
}
}
/*線程獲取到鎖或者發生異常之後都會執行的finally語句塊*/ finally {
/*如果failed爲true,表示獲取鎖失敗,即對應發生異常的情況,
這裏發生異常的情況只有在tryAcquire方法和predecessor方法中可能會拋出異常,此時還沒有獲得鎖,failed=true
那麼執行cancelAcquire方法,該方法用於取消該線程獲取鎖的請求,將該結點的線程狀態改爲CANCELLED,並嘗試移除結點(如果是尾結點)
另外,在超時等待獲取鎖的的方法中,如果超過時間沒有獲取到鎖,也會調用該方法
如果failed爲false,表示獲取到了鎖,那麼該方法直接結束,繼續往下執行;*/
if (failed)
//取消獲取鎖請求,將當前結點從隊列中移除,
cancelAcquire(node);
}
}
/**
* 位於Node結點類中的方法
* 返回上一個結點,或在 null 時引發 NullPointerException。 當前置不能爲空時使用。 空檢查可以取消,表示此異常無代碼層面的意義,但可以幫助 VM?所以這個異常到底有啥用?
*
* @return 此結點的前驅
*/
final Node predecessor() throws NullPointerException {
//獲取前驅
Node p = prev;
//如果爲null,則拋出異常
if (p == null)
throw new NullPointerException();
else
//返回前驅
return p;
}
/**
* head指向node新結點,該方法是在tryAcquire獲取鎖之後調用,不會產生線程安全問題
*
* @param node 新結點
*/
private void setHead(Node node) {
head = node;
//新結點的thread和prev屬性置空
//即丟棄原來的頭結點,新結點成爲哨兵結點,內部線程出隊
//設置裏雖然線程引用置空了,但是一般在tryAcquire方法中軌記錄獲取到鎖的線程,因此不擔心找不到是哪個線程獲取到了鎖
//這裏也能看出,哨兵結點或許也可以叫做"獲取到鎖的結點"
node.thread = null;
node.prev = null;
}
5.3.3.1 shouldParkAfterFailedAcquire結點是否應該掛起
shouldParkAfterFailedAcquire方法在沒有獲取到鎖之後調用,用於判斷當前結點是否需要被掛起。大概步驟如下:
- 如果前驅結點已經是SIGNAL(-1)狀態,即表示當前結點可以掛起,返回true,方法結束;
- 否則,如果前驅結點狀態大於0,即 Node.CANCELLED,表示前驅結點放棄了鎖的等待,那麼由該前驅向前查找,直到找到一個狀態小於等於0的結點,當前結點排在該結點後面,返回false,方法結束;
- 否則,前驅結點的狀態既不是SIGNAL(-1),也不是CANCELLED(1),嘗試CAS設置前驅結點的狀態爲SIGNAL(-1),返回false,方法結束!
只有前驅結點狀態爲SIGNAL時,當前結點才能安心掛起,否則一直自旋!
從這裏能看出來,一個結點的SIGNAL狀態一般都是由它的後繼結點設置的,但是這個狀態卻是表示後繼結點的狀態,表示的意思就是前驅結點如果釋放了鎖,那麼就有義務喚醒後繼結點!
/**
* 檢測當前結點(線程)是否應該被掛起
*
* @param pred 該結點的前驅
* @param node 該結點
* @return 如果前驅結點已經是SIGNAL狀態,當前結點才能掛起,返回true;否則,可能會查找新的前驅結點或者嘗試將前驅結點設置爲SIGNAL狀態,返回false
*/
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
//獲取 前取的waitStatus_等待狀態
//回顧創建結點時候,並沒有給waitStatus賦值,因此每一個結點最開始的時候waitStatus的值都爲0
int ws = pred.waitStatus;
/*如果前驅結點已經是SIGNAL狀態,即表示當前結點可以掛起*/
if (ws == Node.SIGNAL)
return true;
/*如果前驅結點狀態大於0,即 Node.CANCELLED 表示前驅結點放棄了鎖的等待*/
if (ws > 0) {
/*由該前驅向前查找,直到找到一個狀態小於等於0的結點(即沒有被取消的結點),當前結點成爲該結點的後驅,這一步很重要,可能會清理一段被取消了的結點,並且如果該前驅釋放了鎖,還會喚醒它的後繼,保持隊列活性*/
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
}
/*否則,前驅結點的狀態既不是SIGNAL(-1),也不是CANCELLED(1)*/
else {
/*前驅結點的狀態CAS設置爲SIGNAL(-1),可能失敗,但沒關係,因爲失敗之後會一直循環*/
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
//返回false,表示當前結點不能掛起
return false;
}
5.3.3.2 parkAndCheckInterrupt掛起線程&判斷中斷狀態
shouldParkAfterFailedAcquire方法返回true之後,將會調用parkAndCheckInterrupt方法掛起線程並且後續判斷中斷狀態,分兩步:
- 使用LockSupport.park(this)掛起該線程,不再執行後續的步驟、代碼。直到該線程被中斷或者被喚醒(unpark)!
- 如果該線程被中斷或者喚醒,那麼返回Thread.interrupted()方法的返回值,該方法用於判斷前線程的中斷狀態,並且清除該中斷狀態,即,如果該線程因爲被中斷而喚醒,則中斷狀態爲true,將中斷狀態重置爲false,並返回true,如果該線程不是因爲中斷被喚醒,則中斷狀態爲false,並返回false。
/**
* 掛起線程,在線程返回後返回中斷狀態
*
* @return 如果因爲線程中斷而返回,而返回true,否則返回false
*/
private final boolean parkAndCheckInterrupt() {
/*1)使用LockSupport.park(this)掛起該線程,不再執行後續的步驟、代碼。直到該線程被中斷或者被喚醒(unpark)*/
LockSupport.park(this);
/*2)如果該線程被中斷或者喚醒,那麼返回Thread.interrupted()方法的返回值,
該方法用於判斷前線程的中斷狀態,並且清除該中斷狀態,即,如果該線程因爲被中斷而喚醒,則中斷狀態爲true,將中斷狀態重置爲false,並返回true,注意park方法被中斷時不會拋出異常!
如果該線程不是因爲中斷被喚醒,則中斷狀態爲false,並返回false*/
return Thread.interrupted();
}
5.3.3.3 finally代碼塊
在acquireQueued方法中,具有一個finally代碼塊,那麼無論try中發生了什麼,finally代碼塊都會執行的。在acquire獨佔式不可中斷獲取鎖的方法中,執行finally的只有兩種情況:
- 當前結點(線程)最終獲取到了鎖,此時會進入finally,而在獲取到鎖之後會設置failed = false。
- 在try中發生了異常,此時直接跳到finally中。這裏發生異常的情況只可能在tryAcquire或predecessor方法中發生,然後直接進入finally代碼塊中,此時還沒有獲得鎖,failed=true!
a) tryAcquire方法是我們自己實現的,拋出什麼異常由我們來定,就算拋出異常一般也不會在acquireQueued中拋出,可能在最開始調用tryAcquire時就拋出了。
b) predecessor方法中,會檢查如果前驅結點爲null則拋出NullPointerException。但是註釋中又說這個檢查無代碼層面的意義,或許是這個異常永遠不會拋出?
finally代碼塊中的邏輯爲:
- 如果failed = true,表示沒有獲取鎖而進行finally,即發生了異常。那麼執行cancelAcquire方法取消當前結點線程獲取鎖的請求,acquireQueued方法結束,然後拋出異常。
- 如果failed = false,表示已經獲取到了鎖,那麼實際上finally中什麼都不會執行。acquireQueued方法結束,返回interrupted—是否被中斷標誌。
綜上所述,在acquire獨佔式不可中斷獲取鎖的方法中,大部分情況在finally中都是什麼也不幹就返回了,或者說拋出異常的情況基本沒有,因此cancelAcquire方法基本不考慮。
但是在可中斷獲取鎖或者超時獲取鎖的方法中,執行到cancelAcquire方法的情況還是比較常見的。因此將cancelAcquire方法的源碼分析放到可中斷獲取鎖方法的源碼分析部分!
5.3.4 selfInterrupt安全中斷
selfInterrupt是acquire中最後可能調用的一個方法,顧名思義,用於安全的中斷,什麼意思呢,就是根據!tryAcquire和acquireQueued返回值判斷是否需要設置中斷標誌位。
只有tryAcquire嘗試失敗,並且acquireQueued方法true時,才表示該線程是被中斷過了的,但是在parkAndCheckInterrupt裏面判斷中斷標誌位之後又重置的中斷標誌位(interrupted方法會重置中斷標誌位)。
雖然看起來沒啥用,但是本着負責的態度,還是將中斷標誌位記錄下來。那麼此時重新設置該線程的中斷標誌位爲true。
/**
* 中斷當前線程,由於此時當前線程出於運行態,因此只會設置中斷標誌位,並不會拋出異常
*/
static void selfInterrupt() {
Thread.currentThread().interrupt();
}
5.4 release獨佔式鎖釋放
當前線程獲取到鎖並執行了相應邏輯之後,就需要釋放鎖,使得後續結點能夠繼續獲取鎖。通過調用AQS的release(int arg)模版方法可以獨佔式的釋放鎖,在該方法大概步驟如下:
- 嘗試使用tryRelease(arg)釋放鎖,該方法在最開始我們就講過,是自己實現的方法,通常來說就是將state值爲0或者減少、清除當前獲得鎖的線程等等,如果符合自己的邏輯,鎖釋放成功則返回true,否則返回false;
- 如果tryRelease釋放成功返回true,判斷如果head不爲null且head的狀態不爲0,那麼嘗試調用unparkSuccessor方法喚醒頭結點之後的一個非取消狀態(非CANCELLED狀態)的後繼結點,讓其可以進行鎖獲取。返回true,方法結束;
- 如果tryRelease釋放失敗,那麼返回false,方法結束。
/**
* 獨佔式的釋放同步狀態
*
* @param arg 參數
* @return 釋放成功返回true, 否則返回false
*/
public final boolean release(int arg) {
/*tryRelease釋放同步狀態,該方法是自己重寫實現的方法
釋放成功將返回true,否則返回false或者自己實現的邏輯*/
if (tryRelease(arg)) {
//獲取頭結點
Node h = head;
//如果頭結點不爲null並且狀態不等於0
if (h != null && h.waitStatus != 0)
/*那麼喚醒頭結點的一個出於等待鎖狀態的後繼結點
* 該方法在acquire中已經講過了
* */
unparkSuccessor(h);
return true;
}
return false;
}
5.4.1 unparkSuccessor喚醒後繼結點
unparkSuccessor用於喚醒參數結點的某個非取消的後繼結點,該方法在很多地方法都被調用,大概步驟:
- 如果當前結點的狀態小於0,那麼CAS設置爲0,表示後繼結點可以繼續嘗試獲取鎖。
- 如果當前結點的後繼s爲null或者狀態爲取消CANCELLED,則將s先指向null;然後從tail開始到node之間倒序向前查找,找到離tail最近的非取消結點賦給s。需要從後向前遍歷,因爲同步隊列只保證結點前驅關係的正確性。
- 如果s不爲null,那麼狀態肯定不是取消CANCELLED,則直接喚醒s的線程,調用LockSupport.unpark方法喚醒,被喚醒的結點將從被park的位置繼續執行!
/**
* 喚醒指定結點的後繼結點
*
* @param node 指定結點
*/
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
/*
* 1) 如果當前結點的狀態小於0,那麼CAS設置爲0,表示後繼結點線程可以先嚐試獲鎖,而不是直接掛起。
* */
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
//先獲取node的直接後繼
Node s = node.next;
/*
* 2) 如果s爲null或者狀態爲取消CANCELLED,則從tail開始到node之間倒序向前查找,找到離tail最近的非取消結點賦給s。
* */
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
/*
* 3)如果s不爲null,那麼狀態肯定不是取消CANCELLED,則直接喚醒s的線程,調用LockSupport.unpark方法喚醒,被喚醒的結點將從被park的位置向後執行!
* */
if (s != null)
LockSupport.unpark(s.thread);
}
5.5 acquirelnterruptibly獨佔式可中斷獲取鎖
在JDK1.5之前,當一個線程獲取不到鎖而被阻塞在synchronized之外時,如果對該線程進行中斷操作,此時該線程的中斷標誌位會被修改,但線程依舊會阻塞在synchronized上,等待着獲取鎖,即無法響應中斷。
上面分析的獨佔式獲取鎖的方法acquire,同樣是不會響應中斷的。但是AQS提供了另外一個acquireInterruptibly模版方法,調用該方法的線程在等待獲取鎖時,如果當前線程被中斷,會立刻返回,並拋出InterruptedException。
public final void acquireInterruptibly(int arg)
throws InterruptedException {
//如果當前線程被中斷,直接拋出異常
if (Thread.interrupted())
throw new InterruptedException();
//嘗試獲取鎖
if (!tryAcquire(arg))
//如果沒獲取到,那麼調用AQS 可被中斷的方法
doAcquireInterruptibly(arg);
}
5.5.1 doAcquireInterruptibly獨佔式可中斷獲取鎖
doAcquireInterruptibly會首先判斷線程是否是中斷狀態,如果是則直接返回並拋出異常其他不步驟和獨佔式不可中斷獲取鎖基本原理一致,還有一點的區別就是在後續掛起的線程因爲線程被中斷而返回時的處理方式不一樣:獨佔式不可中斷獲取鎖僅僅是記錄該狀態,interrupted = true,緊接着又繼續循環獲取鎖;獨佔式可中斷獲取鎖則直接拋出異常,因此會直接跳出循環去執行finally代碼塊。
/**
* 獨佔可中斷式的鎖獲取
*
* @param arg 參數
*/
private void doAcquireInterruptibly(int arg)
throws InterruptedException {
//同樣調用addWaiter將當前線程構造成結點加入到同步隊列尾部
final Node node = addWaiter(Node.EXCLUSIVE);
//獲取鎖失敗標誌,默認爲true
boolean failed = true;
try {
/*和獨佔式不可中斷方法acquireQueued一樣,循環獲取鎖*/
for (; ; ) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
/*
* 這裏就是區別所在,獨佔不可中斷式方法acquireQueued中
* 如果線程被中斷,此處僅僅會記錄該狀態,interrupted = true,緊接着又繼續循環獲取鎖
*
* 但是在該獨佔可中斷式的鎖獲取方法中
* 如果線程被中斷,此處直接拋出異常,因此會直接跳出循環去執行finally代碼塊
* */
throw new InterruptedException();
}
}
/*獲取到鎖或者拋出異常都會執行finally代碼塊*/
finally {
/*如果獲取鎖失敗。可能就是線程被中斷了,那麼執行cancelAcquire方法取消該結點對鎖的請求,該線程結束*/
if (failed)
cancelAcquire(node);
}
}
5.5.2 finally代碼塊
在doAcquireInterruptibly方法中,具有一個finally代碼塊,那麼無論try中發生了什麼,finally代碼塊都會執行的。在acquireInterruptibly獨佔式可中斷獲取鎖的方法中,執行finally的只有兩種情況:
- 當前結點(線程)最終獲取到了鎖,此時會進入finally,而在獲取到鎖之後會設置failed = false。
- 在try中發生了異常,此時直接跳到finally中,這裏發生異常的情況可能在tryAcquire、predecessor方法中,更加有可能的原因是因爲線程被中斷而拋出InterruptedException異常,然後直接進入finally代碼塊中,此時還沒有獲得鎖,failed=true!
a) tryAcquire方法是我們自己實現的,拋出什麼異常由我們來定,就算拋出異常一般也不會在doAcquireInterruptibly中拋出,可能在最開始調用tryAcquire時就拋出了。
b) predecessor方法中,會檢查如果前驅結點爲null則拋出NullPointerException。但是註釋中又說這個檢查無代碼層面的意義,或許是這個異常永遠不會拋出?
c) 根據doAcquireInterruptibly邏輯,如果線程在掛起過程中被中斷,那麼將主動拋出InterruptedException異常,這也是被稱爲“可中斷”的邏輯
finally代碼塊中的邏輯爲:
- 如果failed = true,表示沒有獲取鎖而進行finally,即發生了異常。那麼執行cancelAcquire方法取消當前結點線程獲取鎖的請求,doAcquireInterruptibly方法結束,拋出異常!
- 如果failed = false,表示已經獲取到了鎖,那麼實際上finally中什麼都不會執行,doAcquireInterruptibly方法結束。
5.5.2.1 cancelAcquire取消獲取鎖請求
由於獨佔式可中斷獲取鎖的方法中,線程被中斷而拋出異常的情況比較常見,因此這裏分析finally中cancelAcquire的源碼。cancelAcquire方法用於取消結點獲取鎖的請求,參數爲需要取消的結點node,大概步驟爲:
- node記錄的線程thread置爲null
- 跳過已取消的前置結點。由node向前查找,直到找到一個狀態小於等於0的結點pred (即找一個沒有取消的結點),更新node.prev爲找到的pred。
- node的等待狀態waitStatus置爲CANCELLED,即取消請求鎖。
- 如果node是尾結點,那麼嘗試CAS更新tail指向pred,成功之後繼續CAS設置pred.next爲null。
- 否則,說明node不是尾結點或者CAS失敗(可能存在對尾結點的併發操作):
a) 如果node不是head的後繼 並且 (pred的狀態爲SIGNAL或者將pred的waitStatus置爲SIGNAL成功) 並且 pred記錄的線程不爲null。那麼設置pred.next指向node.next。最後node.next指向node自己。
b) 否則,說明node是head的後繼 或者pred狀態設置失敗 或者 pred記錄的線程爲null。那麼調用unparkSuccessor喚醒node的一個沒取消的後繼結點。最後node.next指向node自己。
/**
* 取消指定結點獲取鎖的請求
*
* @param node 指定結點
*/
private void cancelAcquire(Node node) {
// Ignore if node doesn't exist
if (node == null)
return;
/*1 node記錄的線程thread置爲null*/
node.thread = null;
/*2 類似於shouldParkAfterFailedAcquire方法中查找有效前驅的代碼:
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
這裏同樣由node向前查找,直到找到一個狀態小於等於0的結點(即沒有被取消的結點),作爲前驅
但是這裏只更新了node.prev,沒有更新pred.next*/
Node pred = node.prev;
while (pred.waitStatus > 0)
node.prev = pred = pred.prev;
//predNext記錄pred的後繼,後續CAS會用到。
Node predNext = pred.next;
/*3 node的等待狀態設置爲CANCELLED,即取消請求鎖*/
node.waitStatus = Node.CANCELLED;
/*4 如果當前結點是尾結點,那麼嘗試CAS更新tail指向pred,成功之後繼續CAS設置pred.next爲null。*/
if (node == tail && compareAndSetTail(node, pred)) {
//新尾結點pred的next結點設置爲null,即使失敗了也沒關係,說明有其它新入隊線程或者其它取消線程更新掉了。
compareAndSetNext(pred, predNext, null);
}
/*5 否則,說明node不是尾結點或者CAS失敗(可能存在對尾結點的併發操作),這種情況要做的事情是把pred和node的後繼非取消結點拼起來。*/
else {
int ws;
/*5.1 如果node不是head的後繼 並且 (pred的狀態爲SIGNAL或者將pred的waitStatus置爲SIGNAL成功) 並且 pred記錄的線程不爲null。
那麼設置pred.next指向node.next。這裏沒有設置prev,但是沒關係。
此時pred的後繼變成了node的後繼—next,後續next結點如果獲取到鎖,那麼在shouldParkAfterFailedAcquire方法中查找有效前驅時,
也會找到這個沒取消的pred,同時將next.prev指向pred,也就設置了prev關係了。
*/
if (pred != head &&
((ws = pred.waitStatus) == Node.SIGNAL ||
(ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
pred.thread != null) {
//獲取next結點
Node next = node.next;
//如果next結點存在且未被取消
if (next != null && next.waitStatus <= 0)
//那麼CAS設置perd.next指向node.next
compareAndSetNext(pred, predNext, next);
}
/*5.2 否則,說明node是head的後繼 或者pred狀態設置失敗 或者 pred記錄的線程爲null。
*
* 此時需要調用unparkSuccessor方法嘗試喚醒node結點的後繼結點,因爲node作爲head的後繼結點是唯一有資格取嘗試獲取鎖的結點。
* 如果外部線程A釋放鎖,但是還沒有調用unpark喚醒node的時候,此時node被中斷或者發生異常,這時node將會調用cancelAcquire取消,結點內部的記錄線程變成null,
* 此時就是算A線程的unpark方法執行,也只是LockSupport.unpark(null)而已,也就不會喚醒任何結點了
* 那麼node後面的結點也不會被喚醒了,隊列就失活了;如果在這種情況下,在node將會調用cancelAcquire取消的代碼中
* 調用一次unparkSuccessor,那麼將喚醒被取消結點的後繼結點,讓後繼結點可以嘗試獲取鎖,從而保證隊列活性!
*
* 前面對node進行取消的代碼中,並沒有將node徹底移除隊列,
* 而被喚醒的結點會嘗試獲取鎖,而在在獲取到鎖之後,在
* setHead(node);
* p.next = null; // help GC
* 部分,可能將這些被取消的結點清除
* */
else {
unparkSuccessor(node);
}
/*最後node.next指向node自身,方便後續GC時直接銷燬無效結點
同時也是爲了Condition的isOnSyncQueue方法,判斷一個原先屬於條件隊列的結點是否轉移到了同步隊列。
因爲同步隊列中會用到結點的next域,取消結點的next也有值的話,可以斷言next域有值的結點一定在同步隊列上。
這裏也能看出來,遍歷的時候應該採用倒序遍歷,否則採用正序遍歷可能出現死循環*/
node.next = node;
}
}
5.5.2.2 cancelAcquire案例演示
設一個同步隊列結構如下,有ABCDE五個線程調用acquireInterruptibly方法爭奪鎖,並且BCDE線程都是因爲獲取不到鎖而導致的阻塞。
我們來看看幾種情況下cancelAcquire方法怎麼處理的:
如果此時線程D被中斷,那麼拋出異常進入finally代碼塊,屬於node不是尾結點,node不是head的後繼的情況,如下圖:
在cancelAcquire方法之後的結構如下:
如果此時線程E被中斷,那麼拋出異常進入finally代碼塊,屬於node是尾結點的情況,如下圖:
在cancelAcquire方法之後的結構如下:
如果此時進來了兩個新線程F、G,並且又都被掛起了,那麼此時同步隊列結構如下圖:
可以看到,實際上該隊列出現了分叉,這種情況在同步隊列中是很常見的,因爲被取消的結點並沒有主動去除自己的prev引用。那麼這部分被取消的結點無法被刪除嗎,其實是可以的,只不過需要滿足一定的條件結構!
如果此時線程B被中斷,那麼拋出異常進入finally代碼塊,屬於node不是尾結點,node是head的後繼的情況,如下圖:
在cancelAcquire方法之後的結構如下:
注意在這種情況下,node還會調用unparkSuccessor方法喚醒後繼結點C,讓C嘗試獲取鎖,如果假設此時線程A的鎖還沒有使用完畢,那麼此時C肯定不能獲取到鎖。
但是C也不是什麼都沒做,C在被喚醒之後獲得CPU執行權的那段時間裏,在doAcquireInterruptibly方法的for循環中,改變了一些引用關係。
它會判斷自己是否可以被掛起,此時它的前驅被取消了waitStatus=1,明顯不能,因此會繼續向前尋找有效的前驅,具體的過程在前面的“acquire- acquireQueued”部分有詳解,最終C被掛起之後的結構如下:
可以看到C最終和head結點直接鏈接了起來,但是此時被取消的B由於具有prev引用,因此還沒有被GC,不要急,這是因爲還沒到指定結構,到了就自然會被GC了。
如果此時線程A的資源使用完畢,那麼首先釋放鎖,然後會嘗試喚醒一個沒有取消的後繼線程,明顯選擇C。如果在A釋放鎖之後,調用LockSupport.unpark方法喚醒C之前,C被先一步因中斷而喚醒了。此時C拋出異常,不會再去獲得鎖,而是去finally執行cancelAcquire方法去了,此時還是屬於node不是尾結點,node是head的後繼的情況,如下圖:
那麼在C執行完cancelAcquire方法之後的結構如下:
如果此時線程A又獲取到了CPU的執行權,執行LockSupport.unpark,但此時結點C因爲被中斷而取消,其內部記錄的線程變量變成了null,LockSupport.unpark(null),將會什麼也不做。那麼這時隊列豈不是失活了?其實並沒有!
此時,cancelAcquire方法中的“node不是尾結點,node是head的後繼”這種情況下的unparkSuccessor方法就非常關鍵了。該方法用於喚醒被取消結點C的一個沒被取消的後繼結點F,讓其嘗試獲取鎖,這樣就能保證隊列不失活。
F被喚醒之後,會判斷是否能夠休眠,明顯不能,因爲前驅node的狀態爲1,此時經過循環中一系列方法的操作,會變成如下結構:
明顯結點F是head的直接後繼,可以獲取鎖。
在獲取鎖成功之後,F會將自己設置爲新的head,此時又會改變一些引用關係,即將F與前驅結點的prev和next關係都移除:
setHead(node);
p.next = null; // help GC
引用關係改變之後的結構下:
可以看到,到這一步,纔會真正的將哪些無效結點刪除,被GC回收。那麼,需要真正刪除一個結點需要有什麼條件?條件就是:如果某個結點獲取到了鎖,那麼該結點的前驅以及和該結點前驅相關的結點都將會被刪除!
比如此時F線程執行完了,下一個就是G,那麼G獲得鎖之後,F將會被刪除,最終結構如下:
5.6 tryAcquireNanos獨佔式超時獲取鎖
獨佔式超時獲取鎖tryAcquireNanos模版方法可以被視作響應中斷獲取鎖acquireInterruptibly方法的“增強版”,支持中斷,支持超時時間!
/**
* 獨佔式超時獲取鎖,支持中斷
*
* @param arg 參數
* @param nanosTimeout 超時時間,納秒
* @return 是否獲取鎖成功
* @throws InterruptedException 如果被中斷,則拋出InterruptedException異常
*/
public final boolean tryAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
//如果當前線程被中斷,直接拋出異常
if (Thread.interrupted())
throw new InterruptedException();
//同樣調用tryAcquire嘗試獲取鎖,如果獲取成功則直接返回true
//否則調用doAcquireNanos方法掛起指定一段時間,該短時間內獲取到了鎖則返回true,超時還未獲取到鎖則返回false
return tryAcquire(arg) ||
doAcquireNanos(arg, nanosTimeout);
}
5.6.1 doAcquireNanos獨佔式超時獲取鎖
doAcquireNanos(int arg,long nanosTimeout)方法在支持響應中斷的基礎上, 增加了超時獲取的特性。
該方法在自旋過程中,當結點的前驅結點爲頭結點時嘗試獲取鎖,如果獲取成功則從該方法返回,這個過程和獨佔式同步獲取的過程類似,但是在鎖獲取失敗的處理上有所不同。
如果當前線程獲取鎖失敗,則判斷是否超時(nanosTimeout小於等於0表示已經超時),如果沒有超時,重新計算超時間隔nanosTimeout,然後使當前線程等待nanosTimeout納秒(當已到設置的超時時間,該線程會從LockSupport.parkNanos(Objectblocker,long nanos)方法返回)。
如果nanosTimeout小於等於spinForTimeoutThreshold(1000納秒)時,將不會使該線程進行超時等待,而是進入快速的自旋過程。原因在於,非常短的超時等待無法做到十分精確,如果這時再進行超時等待,相反會讓nanosTimeout的超時從整體上表現得反而不精確。 因此,在超時非常短的場景下,AQS會進入無條件的快速自旋而不是掛起線程。
static final long spinForTimeoutThreshold = 1000L;
/**
* 獨佔式超時獲取鎖
*
* @param arg 參數
* @param nanosTimeout 剩餘超時時間,納秒
* @return true 成功 ;false 失敗
* @throws InterruptedException 如果被中斷,則拋出InterruptedException異常
*/
private boolean doAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
//獲取當前的納秒時間
long lastTime = System.nanoTime();
//同樣調用addWaiter將當前線程構造成結點加入到同步隊列尾部
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;
try {
/*和獨佔式不可中斷方法acquireQueued一樣,循環獲取鎖*/
for (; ; ) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return true;
}
/*這裏就是區別所在*/
//如果剩餘超時時間小於0,則退出循環,返回false,表示沒獲取到鎖
if (nanosTimeout <= 0)
return false;
//如果需要掛起 並且 剩餘nanosTimeout大於spinForTimeoutThreshold,即大於1000納秒
if (shouldParkAfterFailedAcquire(p, node)
&& nanosTimeout > spinForTimeoutThreshold)
//那麼調用LockSupport.parkNanos方法將當前線程掛起nanosTimeout
LockSupport.parkNanos(this, nanosTimeout);
//獲取當前納秒,走到這一步可能是線程中途被喚醒了
long now = System.nanoTime();
//計算 新的剩餘超時時間:原剩餘超時時間 - (當前時間now - 上一次計算時的時間lastTime)
nanosTimeout -= now - lastTime;
//lastIme賦值爲本次計算時的時間
lastTime = now;
//如果線程被中斷了,那麼直接拋出異常
if (Thread.interrupted())
throw new InterruptedException();
}
}
/*獲取到鎖、超時時間到了、拋出異常都會執行finally代碼塊*/
finally {
/*如果獲取鎖失敗。可能就是線程被中斷了,那麼執行cancelAcquire方法取消該結點對鎖的請求,該線程結束
* 或者是超時時間到了,那麼執行cancelAcquire方法取消該結點對鎖的請求,將返回false
* */
if (failed)
cancelAcquire(node);
}
}
5.6.2 finally代碼塊
在doAcquireNanos方法中,具有一個finally代碼塊,那麼無論try中發生了什麼,finally代碼塊都會執行的。在tryAcquireNanos獨佔式超時獲取鎖的方法中,執行finally的只有三種情況:
- 當前結點(線程)最終獲取到了鎖,此時會進入finally,而在獲取到鎖之後會設置failed = false。
- 在try中發生了異常,此時直接跳到finally中,這裏發生異常的情況可能在tryAcquire、predecessor方法中,更加有可能的原因是因爲線程被中斷而拋出InterruptedException異常,然後直接進入finally代碼塊中,此時還沒有獲得鎖,failed=true!
a) tryAcquire方法是我們自己實現的,據拋出什麼異常由我們來定,就算拋出異常一般也不會在doAcquireNanos中拋出,可能在最開始調用tryAcquire時就拋出了。
b) predecessor方法中,會檢查如果前驅結點爲null則拋出NullPointerException。但是註釋中又說這個檢查無代碼層面的意義,或許是這個異常永遠不會拋出?
c) 根據doAcquireNanos邏輯,如果線程在掛起過程中被中斷,那麼將主動拋出InterruptedException異常,這也是被稱爲“可中斷”的邏輯。 - 方法的超時時間到了,當前線程還沒有獲取到鎖,那麼,將會跳出循環,直接進入finally代碼塊中,此時還沒有獲得鎖,failed=true!
finally代碼塊中的邏輯爲:
- 如果failed = true,表示沒有獲取鎖而進行finally,可能發生了異常 或者 超時時間到了。那麼執行cancelAcquire方法取消當前結點線程獲取鎖的請求,doAcquireNanos方法結束,拋出異常 或者返回 false。
- 如果failed = false,表示已經獲取到了鎖,那麼實際上finally中什麼都不會執行,doAcquireNanos方法結束,返回true。
5.7 獨佔式獲取/釋放鎖總結
獨佔式的獲取鎖和釋放鎖的方法中,我們需要重寫tryAcquire 和tryRelease 方法。
獨佔式的獲取鎖和釋放鎖時,需要在tryAcquire方法中記錄到底是哪一個線程獲取了鎖。一般使用exclusiveOwnerThread字段(setExclusiveOwnerThread方法)記錄,在tryRelease 方法釋放鎖成功之後清楚該字段的值。
5.7.1 acquire/release流程圖
acquire流程:
release流程:
5.7.2 acquire一般流程
根據在上面的源碼,我們嘗試總結出acquire方法(獨佔式獲取鎖)構建同步隊列的一般流程爲。
首先第一個線程A調用lock方法,此時還沒有線程獲取鎖,那麼線程A在acquire的tryAcquire方法中即獲得了鎖,此時同步隊列還沒有初始化,head和tail都是null。
此時第二個線程B進來了,由於A已經獲取了鎖,此時該線程將會被構造成結點添加到隊列中,enq方法中,第一次循環時,由於tail爲null,因此將會構造一個空結點作爲同步隊列的頭結點和尾結點:
第二次循環時,該結點將會添加到結點尾部,tail指向該結點!
然後在acquireQueued方法中,假設結點自旋沒有獲得鎖,那麼在shouldParkAfterFailedAcquire方法中將會設置前驅結點的waitStatus=-1,然後該結點的線程B將會被掛起:
接下來,如果線程C也嘗試獲取鎖,假設沒有獲取到,那麼此時C也將會被掛起:
從這裏能夠看出來,一個結點的SIGNAL狀態(-1)是它的後繼子結點給它設置的,那多條線程情況下,最有可能的情況爲:
到此acquire一般流程分析完畢!
5.7.3 release一般流程
根據在上面的源碼以上面的圖爲基礎,我們嘗試總結出release方法(獨佔式鎖釋放)的一般流程爲:
假如線程A共享資源使用完畢,調用unlock方法,內部調用了release方法,此時先調用tryRelease 釋放鎖,釋放成功之後調用unparkSuccessor方法,設置head結點狀態爲0,並喚醒head結點的沒有取消的後繼結點(waitStatus不大於0),這裏明顯是B線程結點。resize方法到這裏其實已經結束了,下面就是被喚醒結點的操作。
調用unpark喚醒線程B之後,線程B在parkAndCheckInterrupt方法中繼續執行,首先判斷中斷狀態,記錄是因爲什麼原因被喚醒的,這裏不是因爲中斷而被喚醒,因此返回false,那麼acquireQueued的interrupted字段爲false。
然後線程B在acquireQueued方法中繼續自旋,假設此時B獲取到了鎖,那麼調用setHead方法清除線程記錄,並將B結點設置爲頭結點。這裏清除了結點內部的線程記錄也沒關係,因爲在我們實現tryAcquire方法中一般會記錄是哪個線程獲取了鎖。
當最後一個阻塞結點被喚醒,並且線程E獲取鎖之後,同步隊列的結構如下:
當最後一個線程E共享資源使用完畢調用unlock時,在release中釋放鎖之後,再嘗試利用head喚醒後繼結點時,判斷此時head結點的waitStatus還是等於0,因此不會再調用unparkSuccessor方法。
到此release一般流程分析完畢!
5.8 acquireShared共享式獲取鎖
共享式獲取與獨佔式獲取的區別就是同一時刻是否可以多個線程同時獲取到鎖。
在獨佔鎖的實現中會使用一個exclusiveOwnerThread屬性,用來記錄當前持有鎖的線程。當獨佔鎖已經被某個線程持有時,其他線程只能等待它被釋放後,才能去爭鎖,並且同一時刻只有一個線程能爭鎖成功。
對於共享鎖來說,如果一個線程成功獲取了共享鎖,那麼其他等待在這個共享鎖上的線程就也可以嘗試去獲取鎖,並且極有可能獲取成功。基於共享式實現的組件有CountDownLatch、Semaphore等。
通過調用AQS的acquireShared模版方法方法可以共享式地獲取鎖,同樣該方法不響應中斷。實際上如果看懂了獨佔式獲取鎖的源碼,那麼看共享式獲取鎖的源碼就非常簡單了。大概步驟如下:
- 首先使用tryAcquireShared嘗試獲取鎖,獲取成功(返回值大於等於0)則直接返回;
- 否則,調用doAcquireShared將當前線程封裝爲Node.SHARED模式的Node 結點後加入到AQS 同步隊列的尾部,然後"自旋"嘗試獲取鎖,如果還是獲取不到,那麼最終使用park方法掛起自己等待被喚醒。
/**
* 共享式獲取鎖的模版方法,不響應中斷
*
* @param arg 參數
*/
public final void acquireShared(int arg) {
//嘗試調用tryAcquireShared方法獲取鎖
//獲取成功(返回值大於等於0)則直接返回;
if (tryAcquireShared(arg) < 0)
//失敗則調用doAcquireShared方法將當前線程封裝爲Node.SHARED類型的Node 結點後加入到AQS 同步隊列的尾部,
//然後"自旋"嘗試獲取同步狀態,如果還是獲取不到,那麼最終使用park方法掛起自己。
doAcquireShared(arg);
}
5.8.1 tryAcquireShared嘗試獲取共享鎖
熟悉的tryAcquireShared方法,這個方法我們在最開頭講“AQS的設計”時就提到過,該方法是AQS的子類即我們自己實現的,用於嘗試獲取共享鎖,一般來說就是對state的改變、或者重入鎖的檢查等等,不同的鎖有自己相應的邏輯判斷,這裏不多講,後面講具體鎖的實現的時候(比如CountDownLatch)會講到。
返回int類型的值(比如返回剩餘的state狀態值-資源數量),一般的理解爲:
- 如果返回值小於0,表示當前線程共享鎖失敗;
- 如果返回值大於0,表示當前線程共享鎖成功,並且接下來其他線程嘗試獲取共享鎖的行爲很可能成功;
- 如果返回值等於0,表示當前線程共享鎖成功,但是接下來其他線程嘗試獲取共享鎖的行爲會失敗。
實際上在AQS的實際實現中,即使某時刻返回值等於0,接下來其他線程嘗試獲取共享鎖的行爲也可能會成功。即某線程獲取鎖並且返回值等於0之後,馬上又有線程釋放了鎖,導致實際上可獲取鎖數量大於0,此時後繼還是可以嘗試獲取鎖的。
在AQS的中tryAcquireShared的實現爲拋出異常,因此需要子類重寫:
protected int tryAcquireShared(int arg) {
throw new UnsupportedOperationException();
}
5.8.2 doAcquireShared自旋獲取共享鎖
首次調用tryAcquireShared方法獲取鎖失敗之後,會調用doAcquireShared方法。類似於獨佔式獲取鎖acquire方法中的addWaiter和acquireQueued方法的組合版本!大概步驟如下:
- 調用addWaiter方法,將當前線程封裝爲Node.SHARED模式的Node結點後加入到AQS 同步隊列的尾部,即表示共享模式。
- 後面就是類似於acquireQueued方法的邏輯,結點自旋嘗試獲取共享鎖。如果還是獲取不到,那麼最終使用park方法掛起自己等待被喚醒。
每個結點可以嘗試獲取鎖的要求是前驅結點是頭結點,那麼它本身就是整個隊列中的第二個結點,每個獲得鎖的結點都一定是成爲過頭結點。那麼如果某第二個結點因爲不滿足條件沒有獲取到共享鎖而被掛起,那麼即使後續結點滿足條件也一定不能獲取到共享鎖。
/**
* 自旋嘗試共享式獲取鎖,一段時間後可能會掛起
* 和獨佔式獲取的區別:
* 1 以共享模式Node.SHARED添加結點
* 2 獲取到鎖之後,修改當前的頭結點,並將信息傳播到後續的結點隊列中
*
* @param arg 參數
*/
private void doAcquireShared(int arg) {
/*1 addWaiter方法邏輯,和獨佔式獲取的區別1 :以共享模式Node.SHARED添加結點*/
final Node node = addWaiter(Node.SHARED);
/*2 下面就是類似於acquireQueued方法的邏輯
* 區別在於獲取到鎖之後acquireQueued調用setHead方法,這裏調用setHeadAndPropagate方法
* */
//當前線程獲取鎖失敗的標誌
boolean failed = true;
try {
//當前線程的中斷標誌
boolean interrupted = false;
for (; ; ) {
//獲取前驅結點
final Node p = node.predecessor();
/*當前驅結點是頭結點的時候就會以共享的方式去嘗試獲取鎖*/
if (p == head) {
int r = tryAcquireShared(arg);
/*返回值如果大於等於0,則表示獲取到了鎖*/
if (r >= 0) {
/*和獨佔式獲取的區別2 :修改當前的頭結點,根據傳播狀態判斷是否要喚醒後繼結點。*/
setHeadAndPropagate(node, r);
// 釋放掉已經獲取到鎖的前驅結點
p.next = null;
/*檢查設置中斷標誌*/
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
/*判斷是否應該掛起,以及掛起的方法,和acquireQueued方法的邏輯完全一致,不會響應中斷*/
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
從源碼可以看出,和獨佔式獲取的主要區別爲:
- addWaiter以共享模式Node.SHARED添加結點。
- 獲取到鎖之後,調用setHeadAndPropagate設置行head結點,然後根據傳播狀態判斷是否要喚醒後繼結點。。
5.8.2.1 setHeadAndPropagat設置結點並傳播信息
在結點線程獲取共享鎖成功之後會調用setHeadAndPropagat方法,相比於setHead方法,在設置head之後多執行了一步propagate操作:
- 和setHead方法一樣設置新head結點信息
- 根據傳播狀態判斷是否要喚醒後繼結點。
5.8.2.1.1 doReleaseShared喚醒後繼結點
doReleaseShared用於在共享模式下喚醒後繼結點。關於Node.PROPAGATE的分析,將在下面總結部分列出!
/**
* 共享式獲取鎖的核心方法,嘗試喚醒一個後繼線程,被喚醒的線程會嘗試獲取共享鎖,如果成功之後,則又會有可能調用setHeadAndPropagate,將喚醒傳播下去。
* 獨佔鎖只有在一個線程釋放所之後纔會喚醒下一個線程,而共享鎖在一個線程在獲取到鎖和釋放掉鎖鎖之後,都可能會調用這個方法喚醒下一個線程
* 因爲在共享鎖模式下,鎖可以被多個線程所共同持有,既然當前線程已經拿到共享鎖了,那麼就可以直接通知後繼結點來獲取鎖,而不必等待鎖被釋放的時候再通知。
*/
private void doReleaseShared() {
/*一個死循環,跳出循環的條件就是最下面的break*/
for (; ; ) {
//獲取當前的head,每次循環讀取最新的head
Node h = head;
//如果h不爲null且h不爲tail,表示隊列至少有兩個結點,那麼嘗試喚醒head後繼結點線程
if (h != null && h != tail) {
int ws = h.waitStatus;
//如果頭結點的狀態爲SIGNAL,那麼表示後繼結點需要被喚醒
if (ws == Node.SIGNAL) {
//嘗試CAS設置h的狀態從Node.SIGNAL變成0
//可能存在多線程操作,但是隻會有一條成功
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
//失敗的線程結束本次循環,繼續下一次循環
continue; // loop to recheck cases
//成功的那一條線程會調用unparkSuccessor方法喚醒head的一個沒有取消的後繼結點
//對於一個head,只需要一條線程去喚醒該head的後繼就行了。上面的CAS就是保證unparkSuccessor方法對於一個head只執行一次
unparkSuccessor(h);
}
/*
* 如果h狀態爲0,那說明後繼結點線程已經是喚醒狀態了或者將會被喚醒,不需要該線程來喚醒
* 那麼嘗試設置h狀態從0變成PROPAGATE,如果失敗則繼續下一次循環,此時設置PROPAGATE狀態能保證喚醒操作能夠傳播下去
* 因爲後繼結點成爲頭結點時,在setHeadAndPropagate方法中能夠讀取到原head結點的PROPAGATE狀態<0,從而讓它可以嘗試喚醒後繼結點(如果存在)
* */
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
//失敗的線程結束本次循環,繼續下一次循環
continue; // loop on failed CAS
}
// 執行到這一步說明在上面的判斷中隊列可能只有一個結點,或者unparkSuccessor方法調用完畢,或h狀態爲PROPAGATE(不需要繼續喚醒後繼)
// 再次檢查h是否仍然是最新的head,如果不是的話需要再進行循環;如果是的話說明head沒有變化,退出循環
if (h == head) // loop if head changed
break;
}
}
5.9 reaseShared共享式釋放鎖
共享鎖的釋放是通過調用releaseShared模版方法來實現的。大概步驟爲:
- 調用tryReleaseShared嘗試釋放共享鎖,這裏必須實現爲線程安全。
- 如果釋放了鎖,那麼調用doReleaseShared方法環迅後繼結點,實現喚醒的傳播。
&emsp對於支持共享式的同步組件(即多個線程同時訪問),它們和獨佔式的主要區別就是tryReleaseShared方法必須確保鎖的釋放是線程安全的(因爲既然是多個線程能夠訪問,那麼釋放的時候也會是多個線程的,就需要保證釋放時候的線程安全)。由於tryReleaseShared方法也是我們自己實現的,因此需要我們自己實現線程安全,所以常常採用CAS的方式來釋放同步狀態。
/**
* 共享模式下釋放鎖的模版方法。
* ,如果成功釋放則會調用
*/
public final boolean releaseShared(int arg) {
//tryReleaseShared釋放指鎖
if (tryReleaseShared(arg)) {
//釋放成功,必定調用doReleaseShared嘗試喚醒後繼結點
doReleaseShared();
return true;
}
return false;
}
5.10 acquireSharedInterruptibly共享式可中斷獲取鎖
上面分析的獨佔式獲取鎖的方法acquireShared是不會響應中斷的。但是AQS提供了另外一個acquireSharedInterruptibly模版方法,調用該方法的線程在等待獲取鎖時,如果當前線程被中斷,會立刻返回,並拋出InterruptedException。
/**
* 共享式可中斷獲取鎖模版方法
*
* @param arg 參數
* @throws InterruptedException 線程處於中斷狀態,拋出此異常
*/
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
//最開始就檢查一次,如果當前線程是被中斷狀態,直接拋出異常
if (Thread.interrupted())
throw new InterruptedException();
//嘗試獲取鎖
if (tryAcquireShared(arg) < 0)
//獲取不到就執行doAcquireSharedInterruptibly方法
doAcquireSharedInterruptibly(arg);
}
5.10.1 doAcquireSharedInterruptibly共享式可中斷獲取鎖
該方法內部操作和doAcquireShared差不多,都是自旋獲取共享鎖,有些許區別,就是在後續掛起的線程因爲線程被中斷而返回時的處理方式不一樣。
共享式不可中斷獲取鎖僅僅是記錄該狀態,interrupted = true,緊接着又繼續循環獲取鎖;共享式可中斷獲取鎖則直接拋出異常,因此會直接跳出循環去執行finally代碼塊。
/**
* 以共享可中斷模式獲取。
*
* @param arg 參數
*/
private void doAcquireSharedInterruptibly(int arg)
throws InterruptedException {
/*內部操作和doAcquireShared差不多,都是自旋獲取共享鎖,有些許區別*/
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
for (; ; ) {
final Node p = node.predecessor();
if (p == head) {
int r = tryAcquireShared(arg);
if (r >= 0) {
setHeadAndPropagate(node, r);
p.next = null; // help GC
failed = false;
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
/*
* 這裏就是區別所在,共享不可中斷式方法doAcquireShared中
* 如果線程被中斷,此處僅僅會記錄該狀態,interrupted = true,緊接着又繼續循環獲取鎖
*
* 但是在該共享可中斷式的鎖獲取方法中
* 如果線程被中斷,此處直接拋出異常,因此會直接跳出循環去執行finally代碼塊
* */
throw new InterruptedException();
}
}
/*獲取到鎖或者拋出異常都會執行finally代碼塊*/
finally {
/*如果獲取鎖失敗。那麼是發生異常的情況,可能就是線程被中斷了,執行cancelAcquire方法取消該結點對鎖的請求,該線程結束*/
if (failed)
cancelAcquire(node);
}
}
5.11 tryAcquireSharedNanos共享式超時獲取鎖
共享式超時獲取鎖tryAcquireSharedNanos模版方法可以被視作共享式響應中斷獲取鎖acquireSharedInterruptibly方法的“增強版”,支持中斷,支持超時時間!
/**
* 共享式超時獲取鎖,支持中斷
*
* @param arg 參數
* @param nanosTimeout 超時時間,納秒
* @return 是否獲取鎖成功
* @throws InterruptedException 如果被中斷,則拋出InterruptedException異常
*/
public final boolean tryAcquireSharedNanos(int arg, long nanosTimeout)
throws InterruptedException {
//最開始就檢查一次,如果當前線程是被中斷狀態,直接拋出異常
if (Thread.interrupted())
throw new InterruptedException();
//下面是一個||運算進行短路連接的代碼
//tryAcquireShared嘗試獲取鎖,獲取到了直接返回true
//獲取不到(左邊表達式爲false) 就執行doAcquireSharedNanos方法
return tryAcquireShared(arg) >= 0 ||
doAcquireSharedNanos(arg, nanosTimeout);
}
5.11.1 doAcquireSharedNanos共享式超時獲取鎖
doAcquireSharedNanos (int arg,long nanosTimeout)方法在支持響應中斷的基礎上, 增加了超時獲取的特性。
該方法在自旋過程中,當結點的前驅結點爲頭結點時嘗試獲取鎖,如果獲取成功則從該方法返回,這個過程和共享式式同步獲取的過程類似,但是在鎖獲取失敗的處理上有所不同。
如果當前線程獲取鎖失敗,則判斷是否超時(nanosTimeout小於等於0表示已經超時),如果沒有超時,重新計算超時間隔nanosTimeout,然後使當前線程等待nanosTimeout納秒(當已到設置的超時時間,該線程會從LockSupport.parkNanos(Objectblocker,long nanos)方法返回)。
如果nanosTimeout小於等於spinForTimeoutThreshold(1000納秒)時,將不會使該線程進行超時等待,而是進入快速的自旋過程。原因在於,非常短的超時等待無法做到十分精確,如果這時再進行超時等待,相反會讓nanosTimeout的超時從整體上表現得反而不精確。因此,在超時非常短的場景下,AQS會進入無條件的快速自旋而不是掛起線程。
static final long spinForTimeoutThreshold = 1000L;
/**
* 以共享超時模式獲取。
*
* @param arg 參數
* @param nanosTimeout 剩餘超時時間,納秒
* @return true 成功 ;false 失敗
* @throws InterruptedException 如果被中斷,則拋出InterruptedException異常
*/
private boolean doAcquireSharedNanos(int arg, long nanosTimeout)
throws InterruptedException {
//剩餘超時時間小於等於0的,直接返回
if (nanosTimeout <= 0L)
return false;
//能夠等待獲取的最後納秒時間
final long deadline = System.nanoTime() + nanosTimeout;
//同樣調用addWaiter將當前線程構造成結點加入到同步隊列尾部
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
/*和共享式式不可中斷方法doAcquireShared一樣,自旋獲取鎖*/
for (; ; ) {
final Node p = node.predecessor();
if (p == head) {
int r = tryAcquireShared(arg);
if (r >= 0) {
setHeadAndPropagate(node, r);
p.next = null; // help GC
failed = false;
return true;
}
}
/*這裏就是區別所在*/
//如果新的剩餘超時時間小於0,則退出循環,返回false,表示沒獲取到鎖
nanosTimeout = deadline - System.nanoTime();
if (nanosTimeout <= 0L)
return false;
//如果需要掛起 並且 剩餘nanosTimeout大於spinForTimeoutThreshold,即大於1000納秒
if (shouldParkAfterFailedAcquire(p, node) &&
nanosTimeout > spinForTimeoutThreshold)
//那麼調用LockSupport.parkNanos方法將當前線程掛起nanosTimeout
LockSupport.parkNanos(this, nanosTimeout);
//如果線程被中斷了,那麼直接拋出異常
if (Thread.interrupted())
throw new InterruptedException();
}
}
/*獲取到鎖、超時時間到了、拋出異常都會執行finally代碼塊*/
finally {
/*如果獲取鎖失敗。可能就是線程被中斷了,那麼執行cancelAcquire方法取消該結點對鎖的請求,該線程結束
* 或者是超時時間到了,那麼執行cancelAcquire方法取消該結點對鎖的請求,將返回false
* */
if (failed)
cancelAcquire(node);
}
}
5.12 共享式獲取/釋放鎖總結
我們可以調用acquireShared 模版方法來獲取不可中斷的共享鎖,可以調用acquireSharedInterruptibly模版方法來可中斷的獲取共享鎖,可以調用tryAcquireSharedNanos模版方法來可中斷可超時的獲取共享鎖,在此之前需要重寫tryAcquireShared方法;還可以調用releaseShared模版方法來釋放共享鎖,在此之前需要重寫tryReleaseShared方法。
對於共享鎖來說,由於鎖是可以多個線程同時獲取的。那麼如果一個線程成功獲取了共享鎖,那麼其他等待在這個共享鎖上的線程就也可以嘗試去獲取鎖,並且極有可能獲取成功。因此在一個結點線程釋放共享鎖成功時,必定調用doReleaseShared嘗試喚醒後繼結點,而在一個結點線程獲取共享鎖成功時,也可能會調用doReleaseShared嘗試喚醒後繼結點。
基於共享式實現的組件有CountDownLatch、Semaphore、ReentrantReadWriteLock等。
5.12.1. Node.PROPAGATE簡析
5.12.1.1 出現時機
doReleaseShared方法在線程獲取共享鎖成功之後可能執行,在線程釋放共享鎖成功之後必定執行。
在doReleaseShared方法中,可能會存在將線程狀態設置爲Node.PROPAGATE的情況,然而,整個AQS類中也只有這一處直接涉及到Node.PROPAGATE狀態,並且僅僅是設置,在其他地方卻再也沒見到對該狀態的直接使用。由於該狀態值爲-3,因此可能是在其他方法中對waitStatus大小範圍的判斷的時候將這種情況包括進去了(猜測)!
關於Node.PROPAGATE的直接代碼如下:
else if (ws == 0 &&!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
首先是需要進入到else if分支,然後需要此時ws(源碼中最開始獲取的head結點的引用的狀態—不一定是最新的)狀態爲0,然後嘗試CAS設置該結點的狀態爲Node.PROPAGATE,並且可能失敗,失敗之後直接continue繼續下一次循環。
對於這個Node.PROPAGATE狀態的作用,衆說紛紜,筆者看了很多文章,很多看起來都有道理,但是仔細想想又有些差錯,在此,筆者不做過多個人分析,首先來看看進入else if分支並且ws爲0的情況有哪些!
- 初始情況
假設某個共享鎖的實現允許最多三個線程持有鎖,此時有線程A、B、C均獲取到了鎖,同步隊列中還有一個被掛起的結點線程D在等待鎖的釋放,此時隊列結構如下:
如果此時線程A釋放了鎖,那麼A將會調用doReleaseShared方法,但是明顯A將會進入if代碼塊中,將head的狀態改爲0,同時調用unparkSuccessor喚醒一個後繼線程,這裏明顯是D。此時同步隊列結構爲:
- 情形1
如果此時線程B、C都釋放了鎖,那麼B、C都將會調用doReleaseShared方法,假設它們執行速度差不多,那麼它們都將會進入到else if中,因爲此時head的狀態變成了0,然後它們都會調用CAS將0改成Node.PROPAGATE,此時只會有一條線程成功,另一條會失敗。
這就是 釋放鎖時,進入到else if的一種情況。即多個釋放鎖的結點操作同一個head,那麼最終只有一個結點能夠在if中成功調用unparkSuccessor喚醒後繼,另外的結點都將失敗並最終都會走到else if中去。同理,獲取鎖時也可能由於上面的原因而進入到else if。 - 情形2
如果此時又來了一個新結點E,由於同樣沒有獲取到鎖那麼會調用addWaiter添加到D結點後面成爲新tail結點。
然後結點E會在shouldParkAfterFailedAcquire方法中嘗試將沒取消的前驅D的waitStatus修改爲Node.SIGNAL,然後掛起。
那麼在新結點E執行addWaiter之後,執行shouldParkAfterFailedAcquire之前,此時同步隊列結構爲:
由於A釋放了鎖,那麼線程D會被喚醒,並調用tryAcquireShared獲取了鎖,那麼將會返回0(常見的共享鎖獲取鎖的實現是使用state減去需要獲取的資源數量,這裏A釋放了一把鎖,D又獲取一把鎖,此時剩餘資源—鎖數量剩餘0)。
此時,如果B再釋放鎖,這就出現了“即使某時刻返回值等於0,接下來其他線程嘗試獲取共享鎖的行爲也可能會成功”的情況。即 某線程獲取共享鎖並且返回值等於0之後,馬上又有其他持有鎖的線程釋放了鎖,導致實際上可獲取鎖數量大於0,此時後繼還是可以嘗試獲取鎖的。
上面的是題外話,我們回到正文。如果此時B釋放了鎖,那麼肯定還是會走doReleaseShared方法,由於在初始情形中,head的狀態已經被A修改爲0,此時B還是會走else if ,將狀態改爲Node.PROPAGATE。
我們回到線程D,此時線程D獲取鎖之後會走到setHeadAndPropagate方法中,在進行sheHead方法調用之後,此時結構如下(假設線程E由於資源分配的原因,在此期間效率低下,還沒有將前驅D的狀態改爲-1,或者由於單核CPU線程切換導致線程E一直沒有分配到時間片):
sheHead之後,就會判斷是否需要調用doReleaseShared方法喚醒後繼線程,這裏的判斷條件是:
propagate > 0 || h == null || h.waitStatus < 0 ||(h = head) == null ||
h.waitStatus < 0
根據結構,只有第三個條件h.waitStatus<0滿足,此時線程D就可以調用doReleaseShared喚醒後繼結點,在這個過程中,關鍵的就是線程B將老head的狀態設置爲Node.PROPAGATE,即-2,小於0,此時可以將喚醒傳播下去,否則被喚醒的線程A將因爲不滿足條件而不會調用doReleaseShared方法!
或許這就是所謂的Node.PROPAGATE可能將喚醒傳播下去的考慮到的情況之一?
而在此時獲取鎖的線程D調用doReleaseShared方法時,由於此時head狀態本來就是0,因此直接進入else if將狀態改爲Node.PROPAGATE,表示此時後繼結點不需要喚醒,但是需要將喚醒操作繼續傳播下去。
這也是在獲取鎖時,在doReleaseShared方法中第一次出現某結點作爲head就直接進入到else if的一種情況。
3) 情形3
由於A釋放了鎖,那麼如果D的獲取了鎖,並且方法執行完畢,那麼此時同步隊列結構如下:
此時又來了一個新結點E,由於同樣沒有獲取到鎖那麼會調用addWaiter添加到head
結點後面成爲新tail結點。
然後結點E會在shouldParkAfterFailedAcquire方法中嘗試將沒取消的前驅head的waitStatus修改爲Node.SIGNAL,然後掛起。
那麼在新結點E執行addWaiter之後,執行shouldParkAfterFailedAcquire之前,此時同步隊列結構爲:
此時線程A嘗試釋放鎖,釋放鎖成功後一定會都調用doReleaseShared方法時,由於此時head狀態本來就是0,因此直接進入else if將狀態改爲Node.PROPAGATE,表示此時後繼結點不需要喚醒,但是需要將喚醒操作繼續傳播下去。
這也是在釋放鎖的時候,在doReleaseShared方法中第一次出現某結點作爲head就直接進入到else if的一種情況。
5.12.1.2 總結
下面總結了會走到else if的幾種情況,可能還有更多情形這裏分有分析出來:
- 多線程併發的在doReleaseShared方法中操作同一個head,並且這段時間head沒發生改變。那麼先進來的一條線程能夠將if執行成功,即將狀態置爲0,然後調用unparkSuccessor喚醒後,後續進來的線程由於狀態爲0,那麼只能執行else if。這種情況對於獲取鎖或者釋放鎖的doReleaseShared方法都可能存在!這種情況發生時,在doReleaseShared方法中第一次出現某結點作爲head時,不會進入else if,一定是後續其他線程以同樣的結點作爲頭結點時,纔會進入else if!
- 對於獲取鎖的doReleaseShared方法,有一種在doReleaseShared方法中第一次出現某結點作爲head就直接進入到else if的一種情況。設結點D作爲原隊列的尾結點,狀態值爲0,然後又來了新結點E,在新結點E的線程調用addWaiter之後(加入隊列成爲新tail),shouldParkAfterFailedAcquire之前(沒來得及修改前驅D的狀態爲-1)的這段特殊時間範圍之內,此時結點D的線程獲取到了鎖成爲新頭結點,並且原頭結點狀態值小於0,那麼就會出現 在獲取鎖時調用doReleaseShared並直接進入else if的情況,這種情況的要求極爲苛刻。或許本就不存在,只是本人哪裏的分析出問題了?
- 對於釋放鎖的doReleaseShared方法,有一種在doReleaseShared方法中第一次出現結點某作爲head就直接進入到else if的一種情況。設結點D作爲原隊列的尾結點,此時狀態值爲0,並且已經獲取到了鎖;然後又來了新結點E,在新結點E的線程調用addWaiter之後(加入隊列成爲新tail),shouldParkAfterFailedAcquire之前(沒來得及修改前驅D的狀態爲-1)的這段特殊時間範圍之內,此時結點D的線程釋放了鎖,那麼就會出現 在釋放鎖時調用doReleaseShared並直接進入else if的情況,這種情況的要求極爲苛刻。或許本就不存在,只是本人哪裏的分析出問題了?
那麼根據上面的情況來看,就算沒有else if這個判斷或者如果沒有Node.PROPAGATE這個狀態的設置,最終對於後續結點的喚醒並沒有什麼大的問題,也並不會導致隊列失活。
加上Node.PROPAGATE這個狀態的設置,導致的直接結果是可能會增加doReleaseShared方法調用的次數,但是也會增加無效、無意義喚醒的次數。 在setHeadAndPropagate方法中,判斷是否需要喚醒後繼的源碼註釋中我們能找到這樣的描述:
The conservatism in both of these checks may cause unnecessary wake-ups, but only when there are multiple racing acquires/releases, so most need signals now or soon anyway.
意思就是,這些判斷可能會造成無意義的喚醒,但如果doReleaseShared方法調用的次數比較多的話,相當於多線程爭搶着去喚醒後繼線程,或許可以提升鎖的獲取速度?或者這裏的代碼只是一種更加通用的保證正確的做法?實際上AQS中還有許多這樣可能會造成無意義調用的代碼!
6 鎖的簡單實現
6.1 可重入獨佔鎖的實現
在最開始我們實現了簡單的不可重入獨佔鎖,現在我們嘗試實現可重入的獨佔鎖,實際上也比較簡單!
AQS 的state 狀態值表示線程獲取該鎖的重入次數, 在默認情況下,state的值爲0 表示當前鎖沒有被任何線程持有。當一個線程第一次獲取該鎖時會嘗試使用CAS設置state 的值爲l ,如果CAS 成功則當前線程獲取了該鎖,然後記錄該鎖的持有者爲當前線程。在該線程沒有釋放鎖的情況下第二次獲取該鎖後,狀態值被設置爲2,這就是重入次數爲2。在該線程釋放該鎖時,會嘗試使用CAS 讓狀態值減1,如果減l 後狀態值爲0,則當前線程釋放該鎖。
對於可重入獨佔鎖,獲取了幾次鎖就需要釋放幾次鎖,否則由於鎖釋放不完全而阻塞其他線程!
/**
* @author lx
*/
public class ReentrantExclusiveLock implements Lock {
/**
* 將AQS的實現組合到鎖的實現內部
*/
private static class Sync extends AbstractQueuedSynchronizer {
/**
* 重寫isHeldExclusively方法
*
* @return 是否處於鎖佔用狀態
*/
@Override
protected boolean isHeldExclusively() {
//state是否等於1
return getState() == 1;
}
/**
* 重寫tryAcquire方法,可重入的嘗試獲取鎖
*
* @param acquires 參數,這裏我們沒用到
* @return 獲取成功返回true,失敗返回false
*/
@Override
public boolean tryAcquire(int acquires) {
/*嘗試獲取鎖*/
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
/*獲取失敗,判斷當前獲取鎖的線程是不是本線程*/
else if (getExclusiveOwnerThread() == Thread.currentThread()) {
//如果是,那麼state+1,表示鎖重入了
setState(getState() + 1);
return true;
}
return false;
}
/**
* 重寫tryRelease方法,可重入的嘗試釋放鎖
*
* @param releases 參數,這裏我們沒用到
* @return 釋放成功返回true,失敗返回false
*/
@Override
protected boolean tryRelease(int releases) {
//如果嘗試解鎖的線程不是加鎖的線程,那麼拋出異常
if (Thread.currentThread() != getExclusiveOwnerThread()) {
throw new IllegalMonitorStateException();
}
boolean flag = false;
int oldState = getState();
int newState = oldState - 1;
//如果state變成0,設置當前擁有獨佔訪問權限的線程爲null,返回true
if (newState == 0) {
setExclusiveOwnerThread(null);
flag = true;
}
//重入鎖的釋放,釋放一次state減去1
setState(newState);
return flag;
}
/**
* 返回一個Condition,每個condition都包含了一個condition隊列
* 用於實現線程在指定條件隊列上的主動等待和喚醒
*
* @return 每次調用返回一個新的ConditionObject
*/
Condition newCondition() {
return new ConditionObject();
}
}
/**
* 僅需要將操作代理到Sync實例上即可
*/
private final Sync sync = new Sync();
/**
* lock接口的lock方法
*/
@Override
public void lock() {
sync.acquire(1);
}
/**
* lock接口的tryLock方法
*/
@Override
public boolean tryLock() {
return sync.tryAcquire(1);
}
@Override
public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}
@Override
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
/**
* lock接口的unlock方法
*/
@Override
public void unlock() {
sync.release(1);
}
/**
* lock接口的newCondition方法
*/
@Override
public Condition newCondition() {
return sync.newCondition();
}
public boolean isLocked() {
return sync.isHeldExclusively();
}
public boolean hasQueuedThreads() {
return sync.hasQueuedThreads();
}
}
6.1.1 測試
/**
* @author lx
*/
public class ReentrantExclusiveLockTest {
/**
* 創建鎖
*/
static ReentrantExclusiveLock reentrantExclusiveLock = new ReentrantExclusiveLock();
/**
* 自增變量
*/
static int i;
public static void main(String[] args) throws InterruptedException {
//三條線程
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(3, 3, 1L, TimeUnit.MINUTES,
new LinkedBlockingQueue<>(), Executors.defaultThreadFactory(), new ThreadPoolExecutor.DiscardPolicy());
Runa runa = new Runa();
for (int i1 = 0; i1 < 3; i1++) {
threadPoolExecutor.execute(runa);
}
threadPoolExecutor.shutdown();
while (!threadPoolExecutor.isTerminated()) {
}
//三條線程執行完畢,輸出最終結果
System.out.println(i);
}
/**
* 線程任務,循環50000次,每次i自增1
*/
public static class Runa implements Runnable {
@Override
public void run() {
// lock與unlock註釋時,可能會得到錯誤的結果
// 開啓時每次都會得到正確的結果150000
//支持多次獲取鎖(重入)
reentrantExclusiveLock.lock();
reentrantExclusiveLock.lock();
for (int i1 = 0; i1 < 50000; i1++) {
i++;
}
//獲取了多少次必須釋放多少次
reentrantExclusiveLock.unlock();
reentrantExclusiveLock.unlock();
}
}
}
6.2 可重入共享鎖的實現
自定義一個共享鎖,共享鎖的數量可以自己指定。默認構造情況下,在同一時刻,最多允許三條線程同時獲取鎖,超過三個線程的訪問將被阻塞。
我們必須重寫tryAcquireShared(int args)方法和tryReleaseShared(int args)方法。由於是共享式的獲取,那麼在對同步狀態state更新時,兩個方法中都需要使用CAS方法compareAndSet(int expect,int update)做原子性保障。
假設一條線程一次只需要獲取一個資源即表示獲取到鎖。由於同一時刻允許至多三個線程的同時訪問,表明同步資源數爲3,這樣可以設置初始狀態state爲3來代表同步資源,當一個線程進行獲取,status減1,該線程釋放,則status加1,狀態的合法範圍爲0、1和2,其中0表示當前已經有兩個線程獲取了同步資源,此時再有其他線程對同步狀態進行獲取,該線程可能會被阻塞。
最後,將自定義的AQS實現通過內部類的方法聚合到自定義鎖中,自定義鎖還需要實現Lock接口,外部方法的內部實現直接調用對應的模版方法即可。
這裏一條線程可以獲取多次共享鎖,但是同時必須釋放多次共享鎖,否則可能由於鎖資源的減少,導致效率低下甚至死鎖(可以使用tryLock避免)!
public class ShareLock implements Lock {
/**
* 默認構造器,默認共享資源3個
*/
public ShareLock() {
sync = new Sync(3);
}
/**
* 指定資源數量的構造器
*/
public ShareLock(int num) {
sync = new Sync(num);
}
private static class Sync extends AbstractQueuedSynchronizer {
Sync(int num) {
if (num <= 0) {
throw new RuntimeException("鎖資源數量需要大於0");
}
setState(num);
}
/**
* 重寫tryAcquireShared獲取共享鎖
*/
@Override
protected int tryAcquireShared(int arg) {
/*一般的思想*/
/*//獲取此時state
int currentState = getState();
//獲取剩餘state
int newState = currentState - arg;
//如果剩餘state小於0則直接返回負數
//否則嘗試更新state,更新成功就說明獲取成功,返回大於等於0的數
return newState < 0 ? newState : compareAndSetState(currentState, newState) ? newState : -1;*/
/*更好的思想
* 在上面的實現中,如果剩餘state值大於0,那麼只嘗試CAS一次,如果失敗就算沒有獲取到鎖,此時該線程會進入同步隊列
* 在下面的實現中,如果剩餘state值大於0,那麼如果嘗試CAS更新不成功,會在for循環中重試,直到剩餘state值小於0或者更新成功
*
* 兩種方法的不同之處在於,對CAS操作是否進行重試,這裏建議第二種
* 因爲可能會有多個線程同時獲取多把鎖,但是由於CAS只能保證一次只有一個線程成功,因此其他線程必定失敗
* 但此時,實際上還是存在剩餘的鎖沒有被獲取完畢的,因此讓其他線程重試,相比於直接加入到同步隊列中,對於鎖的利用率更高!
* */
for (; ; ) {
int currentState = getState();
int newState = currentState - arg;
if (newState < 0 || compareAndSetState(currentState, newState)) {
return newState;
}
}
}
/**
* 重寫tryReleaseShared釋放共享鎖
*
* @param arg 參數
* @return 成功返回true 失敗返回false
*/
@Override
protected boolean tryReleaseShared(int arg) {
//只能成功
for (; ; ) {
int currentState = getState();
int newState = currentState + arg;
if (compareAndSetState(currentState, newState)) {
return true;
}
}
}
}
/**
* 內部初始化一個sync對象,此後僅需要將操作代理到這個Sync對象上即可
*/
private final Sync sync;
/*下面都是調用模版方法*/
@Override
public void lock() {
sync.acquireShared(1);
}
@Override
public void lockInterruptibly() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
@Override
public boolean tryLock() {
return sync.tryAcquireShared(1) >= 0;
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return sync.tryAcquireSharedNanos(1, unit.toNanos(time));
}
@Override
public void unlock() {
sync.releaseShared(1);
}
/**
* @return 沒有實現自定義Condition,單純依靠原始Condition實現是不支持共享鎖的
*/
@Override
public Condition newCondition() {
throw new UnsupportedOperationException();
}
}
6.2.1 測試
public class ShareLockTest {
static final ShareLock lock = new ShareLock();
public static void main(String[] args) {
/*啓動10個線程*/
for (int i = 0; i < 10; i++) {
Worker w = new Worker();
w.setDaemon(true);
w.start();
}
ShareLockTest.sleep(20);
}
/**
* 睡眠
*
* @param seconds 時間,秒
*/
public static void sleep(long seconds) {
try {
TimeUnit.SECONDS.sleep(seconds);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
static class Worker extends Thread {
@Override
public void run() {
/*不停的獲取鎖,釋放鎖
* 最開始獲取了幾個鎖,那麼最後必須釋放幾個鎖
* 否則可能由於鎖資源的減少,導致效率低下甚至死鎖(可以使用tryLock避免)!
* */
while (true) {
/*tryLock測試*/
if (lock.tryLock()) {
System.out.println(Thread.currentThread().getName());
/*獲得鎖之後都會休眠2秒
那麼可以想象,控制檯將很有可能會出現連續三個一起輸出,然後等待2秒,再連續三個一起輸出,然後2秒……*/
ShareLockTest.sleep(2);
lock.unlock();
}
/*lock測試,或許總會出現固定線程獲取鎖,因爲AQS默認是實現是非公平鎖*/
/*lock.lock();
System.out.println(Thread.currentThread().getName());
ShareLockTest.sleep(2);
lock.unlock();*/
}
}
}
}
7 條件隊列
上面講解了AQS對於各種獨佔鎖、共享鎖的實現原理,以及獲取鎖釋放鎖的方法。但是似乎還少了點什麼,那就是Condition條件隊列。
如果我們只有同步隊列,確實可以實現線程同步,但是由於Java的線程實際上還是底層操作系統實現的,它具體分配多長的時間片、具體哪些線程需要等待、什麼時候進入等待、哪些線程能夠獲得鎖,都不是我們我們能夠控制的,這樣很難支持複雜的多線程需求。
而Condition則可用於實現主動的線程的等待、通知機制,是實現可控制的多線程編程中非常重要的一部分!
7.1 Condition概述
7.1.1 Object監視器與Condition
任意一個Java對象,都擁有一與之關聯的唯一的監視器對象monitor(該對象是在HotSpot源碼中使用C++實現的),爲此Java爲每個對象提供了一組監視器方法(定義在java.lang.Object上),主要包括wait()、wait(long timeout)、notify()以及notifyAll()方法,這些方法與synchronized同步關鍵字配合,可以實現等待/通知模式。具體可以看Synchronized的原理:Java中的synchronized的底層實現原理以及鎖升級優化詳解。
Condition(又稱條件變量)接口也提供了類似Object的監視器方法,與Lock配合可以實現等待/通知模式,但是這兩者在使用方式以及功能特性上還是有差別的。Object的監視器方法與Condition接口的對比如下(來自網絡):
Condition可以和任意的鎖對象結合,監視器方法不會再綁定到某個鎖對象上。使用Lock鎖之後,相當於Lock 替代了synchronized方法和語句的使用,Condition替代了Object監視器方法的使用。
在Condition中,Condition對象當中封裝了監視器方法,並用await()替換wait(),用signal()替換notify(),用signalAll()替換notifyAll(),傳統線程的通信方式,Condition都可以實現,這裏注意,Condition是被綁定到Lock上的,要創建一個Lock的Condition必須用newCondition()方法。如果在沒有獲取到鎖前調用了條件變量的await 方法則會拋出java.lang.IllegalMonitorStateException 異常。
synchronized 同時只能與一個共享變量的notify 或wait 方法實現同步,而AQS 的一個鎖可以對應多個條件變量。
Condition的強大之處還在於它可以爲多個線程間建立不同的Condition,使用synchronized/wait()只有一個條件隊列,notifyAll會喚起條件隊列下的所有線程,而使用lock-condition,可以實現多個條件隊列,signalAll只會喚起某個條件隊列下的等待線程。
另外AQS只提供了ConditionObject的實現,並沒有提供獲取Condition的newCondition方法對應的模版方法,需要由AQS的子類來提供具體實現,通常是直接調用ConditionObject的構造器new一個對象返回。一個鎖對象可以多次調用newCondition方法,因此一個鎖對象可以對應多個Condition對象!
7.1.2 常用API方和使用示例
方法名稱 | 描述 |
void await() throws InterruptedException | 當前線程進入等待狀態直到被通知或中斷。該方法返回時,該線程肯定又一次獲取了鎖。 |
void awaitUninterruptibly() | 當前線程進入等待狀態直到被通知,等待過程中不響應中斷。該方法返回時,該線程肯定又一次獲取了鎖。 |
long awaitNanos(long nanosTimeout) throws InterruptedException | 當前線程進入等待狀態直到被通知,中斷,或者超時。返回(超時時間 - 實際返回所用時間)。如果返回值是0或者負數,那麼可以認定已經超時了。該方法返回時,該線程肯定又一次獲取了鎖。 |
boolean await(long time, TimeUnit unit) throws InterruptedException | 當前線程進入等待狀態直到被通知,中斷,或者超時。如果在從此方法返回前檢測到等待時間超時,則返回 false,否則返回 true。該方法返回時,該線程肯定又一次獲取了鎖。 |
boolean awaitUntil(Date deadline) throws InterruptedException | 當前線程進入等待狀態直到被通知,中斷或者超過指定時間點。如果沒有到指定時間就被通知,則返回true,否則返回false。 |
void signal() | 喚醒一個在Condition上等待最久的線程,該線程從等待方法返回前必須獲得與Condition相關聯的鎖 |
void signalAll() | 喚醒所有等待在Condition上的線程,能夠從等待方法返回的線程必須獲得與Condition相關聯的鎖 |
Condition定義了等待/通知兩種類型的方法,當前線程調用這些方法時,需要提前獲取到Condition對象關聯的鎖。Condition對象是由Lock對象(調用Lock對象的newCondition()方法)創建出來的,換句話說,Condition是依賴Lock對象的。
下面示例Condition實現有界同步隊列(生產消費):
/**
* 使用Condition實現有界隊列
*/
public class BoundedQueue<T> {
//數組隊列
private Object[] items;
//添加下標
private int addIndex;
//刪除下標
private int removeIndex;
//當前隊列數據數量
private int count;
//互斥鎖
private Lock lock = new ReentrantLock();
//隊列不爲空的條件
private Condition notEmpty = lock.newCondition();
//隊列沒有滿的條件
private Condition notFull = lock.newCondition();
public BoundedQueue(int size) {
items = new Object[size];
}
//添加一個元素,如果數組滿了,添加線程進入等待狀態,直到有“空位”
public void add(T t) {
lock.lock();
try {
while (count == items.length) {
notFull.await();
}
items[addIndex] = t;
if (++addIndex == items.length) {
addIndex = 0;
}
++count;
//喚醒一個等待刪除的線程
notEmpty.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
//由頭部刪除一個元素,如果數組空,則刪除線程進入等待狀態,知道有新元素加入
public T remove() throws InterruptedException {
lock.lock();
try {
while (count == 0) {
notEmpty.await();
}
Object res = items[removeIndex];
if (++removeIndex == items.length)
removeIndex = 0;
--count;
//喚醒一個等待插入的線程
notFull.signal();
return (T) res;
} finally {
lock.unlock();
}
}
@Override
public String toString() {
return "BoundedQueue{" +
"items=" + Arrays.toString(items) +
", addIndex=" + addIndex +
", removeIndex=" + removeIndex +
", count=" + count +
", lock=" + lock +
", notEmpty=" + notEmpty +
", notFull=" + notFull +
'}';
}
public static void main(String[] args) throws InterruptedException {
BoundedQueue<Object> objectBoundedQueue = new BoundedQueue<>(10);
for (int i = 0; i < 20; i++) {
objectBoundedQueue.add(i);
System.out.println(objectBoundedQueue);
if (i/2==0) {
objectBoundedQueue.remove();
}
}
}
}
首先需要獲得鎖,目的是確保數組修改的可見性和排他性。當數組數量等於數組長度時,
表示數組已滿,則調用notFull.await(),當前線程隨之釋放鎖並進入等待狀態。如果數組數量不等於數組長度,表示數組未滿,則添加元素到數組中,同時通知等待在notEmpty上的線程,數組中已經有新元素可以獲取。
在添加和刪除方法中使用while循環而非if判斷,目的是防止過早或意外的通知,只有條件符合才能夠退出循環。
7.2 條件隊列的結構
每一個AQS對象中包含一個同步隊列,類似的,每個Condition對象中都包含着一個隊列(以下稱爲等待/條件隊列),用來存放調用該Condition對象的await()方法時被阻塞的線程。該隊列是Condition實現等待/通知機制的底層關鍵數據結構。
條件隊列同樣是一個FIFO的隊列,結點的類型直接複用的同步隊列的結點類型—AQS的靜態內部類AbstractQueuedSynchronizer.Node。一個Condition對象的隊列中每個結點包含的線程就是在該Condition對象上等待的線程,那麼如果一個鎖對象獲取了多個Condition對象,就可能會有不同的線程在不同的Condition對象上等待!
如果一個獲取到鎖的線程調用了Condition.await()方法,那麼該線程將會被構造成等待類型爲Node.CONDITION的Node結點加入等待隊列尾部並釋放鎖,加入對應Condition對象的條件隊列尾部並掛起(WAITING)。
如果某個線程中調用某個Condition的signal/signalAll方法,對應Condition對象的條件隊列的結點會轉移到鎖內部的AQS對象的同步隊列中,並且在獲取到鎖之後,對應的線程纔可以繼續恢復執行後續代碼。
ConditionObject中持有條件隊列的頭結點引用firstWaiter和尾結點引用lastWaiter。
public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer
implements java.io.Serializable {
/**
* 同步隊列頭節點
*/
private transient volatile Node head;
/**
* 同步隊列尾節點
*/
private transient volatile Node tail;
/**
* 同步狀態
*/
private volatile int state;
/**
* Node節點的實現
*/
static final class Node {
//……
}
/**
* 位於AQS內部的ConditionObject類,就是Condition的實現
*/
public class ConditionObject implements Condition, java.io.Serializable {
private static final long serialVersionUID = 1173984872572414699L;
/**
* 條件隊列頭結點引用
*/
private transient Node firstWaiter;
/**
* 條件隊列尾結點引用
*/
private transient Node lastWaiter;
//……
}
}
Condition的實現是AQS的內部類ConditionObject,因此每個Condition實例都能夠訪問AQS提供的方法,相當於每個Condition都擁有所屬AQS的引用。
和AQS中的同步隊列不同的是,條件隊列是一個單鏈表,結點之間使用nextWaiter引用維持後繼的關係,並不會用到prev, next屬性,它們的值都爲null,並且沒有哨兵結點,大概結構如下:
如圖所示,Condition擁有首尾結點的引用,而新增結點只需要將原有的尾結點nextWaiter指向它,並且更新尾結點即可。上述結點引用更新的過程並沒有使用CAS保證,原因在於調用await()方法的線程必定是獲取了鎖的線程,也就是說該過程是由鎖來保證線程安全的。
在Object的監視器模型上,一個監視器對象只能擁有一個同步隊列和等待隊列,而JUC中的一個同步組件實例可以擁有一個同步隊列和多個條件隊列,其對應關係如下圖:
7.3 等待機制原理
調用Condition的await()、await(long time, TimeUnit unit)、awaitNanos(long nanosTimeout)、awaitUninterruptibly()、awaitUntil(Date deadline)方法時,會使當前線程構造成一個等待類型爲Node.CONDITION的Node結點加入等待隊列尾部並釋放鎖,同時線程狀態變爲等待狀態(WAITING)。而當從await()方法返回時,當前線程一定獲取了Condition相關聯的鎖。
對於同步隊列和條件隊列這兩個兩個隊列來說,當調用await()方法時,相當於同步隊列的頭結點(獲取了鎖的結點)移動到Condition的等待隊列成爲尾結點(只是一個比喻,實際上並沒有移動)。
AQS的Node的waitStatus使用Node.CONDITION(-2)來表示結點處於等待狀態,如果條件隊列中的結點不是Node.CONDITION狀態,那麼就認爲該結點就不再等待了,需要出隊列。
7.3.1 await()響應中斷等待
調用該方法的線程也一定是成功獲取了鎖的線程,也就是同步隊列中的首結點,如果一個沒有獲得鎖的線程調用此方法,那麼可能會拋出異常!
await方法用於將當前線程構造成結點並加入等待隊列中,並釋放鎖,然後當前線程會進入等待狀態,等待喚醒。
當等待隊列中的結點線程因爲signal、signalAll或者被中斷而喚醒,則會被移動到同步隊列中,然後在await中嘗試獲取鎖。如果是在其他線程調用signal、signalAll方法之前就因爲中斷而被喚醒了,則會拋出InterruptedException。
該方法響應中斷。最終,如果該方法能夠返回,那麼該線程一定是又一次重新獲取到鎖了。
大概步驟爲:
- 最開始就檢查一次,如果當前線程是被中斷狀態,直接拋出異常
- 調用addConditionWaiter方法,將當前線程封裝成Node.CONDITION類型的Node結點鏈接到條件隊列尾部,返回新加的結點,該過程中將移除取消等待的結點。
- 調用fullyRelease方法,一次性釋放當前線程所佔用的所有的鎖(重入鎖),並返回取消時的同步狀態state 值。
- 循環,調用isOnSyncQueue方法判斷結點是否被轉移到了同步隊列中:
a) 如果不在同步隊列中,那麼park掛起當前線程,不在執行後續代碼。
b) 如果被喚醒,那麼調用checkInterruptWhileWaiting檢查線程被喚醒的原因,並且使用interruptMode字段記錄中斷模式。
c) 如果此時線程時中斷狀態,那麼break跳出循環,否則,進行下一次循環判斷。 - 到這一步,結點一定是加入同步隊列中了。那麼使用acquireQueued自旋獲取獨佔鎖,將鎖重入次數原封不動的寫回去。
- 獲取到鎖之後,判斷如果在獲取鎖的等待過程中被中斷,並且之前的中斷模式不爲THROW_IE(可能是0),那麼設置中斷模式爲REINTERRUPT。
- 如果結點後繼不爲null,說明是“在調用signal或者signalAll方法之前就因爲中斷而被喚醒”的情況,發生這種情況時結點是沒有從條件隊列中移除的,此時需要移除。這裏直接調用調用unlinkCancelledWaiters對條件隊列再次進行整體清理。
- 如果中斷模式不爲0,那麼調用reportInterruptAfterWait方法對不同的中斷模式做出處理。
/**
* 位於ConditionObject中的方法
* 當前線程進入等待狀態,直到被通知或中斷
*
* @throws InterruptedException 如果線程被中斷,那麼返回並拋出異常
*/
public final void await() throws InterruptedException {
/*最開始就檢查一次,如果當前線程是被中斷狀態,直接拋出異常*/
if (Thread.interrupted())
throw new InterruptedException();
/*當前線程封裝成Node.CONDITION類型的Node結點鏈接到條件隊列尾部,返回新加的結點*/
Node node = addConditionWaiter();
/*嘗試釋放當前線程所佔用的所有的鎖,並保存當前的鎖狀態*/
int savedState = fullyRelease(node);
//中斷模式,默認爲0 表示沒有中斷,後面會介紹
int interruptMode = 0;
/*循環檢測,如果當前隊列不在同步隊列中,那麼將當前線程繼續掛起,停止執行後續代碼,直到被通知/中斷;
否則,表示已在同步隊列中,直接跳出循環*/
while (!isOnSyncQueue(node)) {
//此處線程阻塞
LockSupport.park(this);
// 走到這一步說明可能是被其他線程通知喚醒了或者是因爲線程中斷而被喚醒
// checkInterruptWhileWaiting檢查線程被喚醒的原因,並且使用interruptMode字段記錄中斷模式
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
//如果是中斷狀態,則跳出循環,這說明中斷狀態也會離開條件隊列加入同步隊列
break;
/*如果沒有中斷,那就是因爲signal或者signalAll方法的調用而被喚醒的,並且已經被加入到了同步隊列中
* 在下一次循環時,將不滿足循環條件,而自動退出循環*/
}
/*
* 到這一步,結點一定是加入同步隊列中了
* 那麼使用acquireQueued自旋獲取獨佔鎖,第二個參數就是最開始釋放鎖時的同步狀態,這裏要將鎖重入次數原封不動的寫回去
* 如果在獲取鎖的等待過程中被中斷,並且之前的中斷模式不爲THROW_IE(可能是0),那麼設置中斷模式爲REINTERRUPT,
* 即表示在調用signal或者signalAll方法之後設置的中斷狀態
* */
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
/*此時已經獲取到了鎖,那麼實際上這個對應的結點就是head結點了
*但是如果線程是 在調用signal或者signalAll方法之前就因爲中斷而被喚醒 的情況時,將結點添加到同步隊列的的時候,並沒有清除在條件隊列中的結點引用
*因此,判斷nextWaiter是否不爲null,如果是則還需要從條件隊列中移除徹底移除這個結點。
* */
if (node.nextWaiter != null)
//這裏直接調用unlinkCancelledWaiters方法移除所有waitStatus不爲CONDITION的結點
unlinkCancelledWaiters();
//如果中斷模式不爲0,那麼調用reportInterruptAfterWait方法對不同的中斷模式做出處理
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
7.3.1.1 addConditionWaiter添加結點到條件隊列
同步隊列的首結點並不會直接加入等待隊列,而是通過addConditionWaite方法把當前線程構造成一個新的結點並將其加入等待隊列中。
addConditionWaiter方法用於將當前線程封裝成Node.CONDITION類型的結點鏈接到條件隊列尾部。大概有兩步:
- 首先獲取條件隊列尾結點,如果尾結點不是等待狀態,那麼調用unlinkCancelledWaiters對整個鏈表做一個清理,清除不是等待狀態的結點;
- 將當前線程包裝成Node.CONDITION類型的Node加入條件隊列尾部,這裏不需要CAS,因爲此時線程已經獲得了鎖,不存在併發的情況。
/**
* 位於ConditionObject中的方法
* 當前線程封裝成Node.CONDITION類型的Node結點鏈接到條件隊列尾部
*
* @return 新添加的結點
*/
private Node addConditionWaiter() {
//獲取條件隊列尾結點t
Node t = lastWaiter;
/*1 如果t的狀態不爲Node.CONDITION,即不是等待狀態了遍歷整個條件隊列鏈表,清除所有不在等待狀態的結點*/
if (t != null && t.waitStatus != Node.CONDITION) {
/*遍歷整個條件隊列鏈表,清除所有不在等待狀態的結點*/
unlinkCancelledWaiters();
//獲取最新的尾結點
t = lastWaiter;
}
/*2 將當前線程包裝成Node.CONDITION類型的Node加入條件隊列尾部
* 這裏不需要CAS,因爲此時線程已經獲得了鎖,不存在併發的情況
* 從這裏也能看出來,條件隊列僅僅是一條通過nextWaiter維持後繼關係的單鏈表,同時不存在類似於同步隊列的哨兵結點
* */
Node node = new Node(Thread.currentThread(), Node.CONDITION);
if (t == null)
firstWaiter = node;
else
t.nextWaiter = node;
lastWaiter = node;
//返回新加結點
return node;
}
7.3.1.2 unlinkCancelledWaiters清除取消等待的結點
unlinkCancelledWaiters方法會從頭開始遍歷整個單鏈表,清除所有取消等待的結點,比較簡單!
/**
* 位於ConditionObject中的方法
* 從頭開始遍歷整個條件隊列鏈表,清除所有不在等待狀態的結點
*/
private void unlinkCancelledWaiters() {
//t用來記錄當前結點的引用,從頭結點開始
Node t = firstWaiter;
//trail用來記錄上一個非取消(Node.CONDITION)結點的引用
Node trail = null;
/*頭結點不爲null,則遍歷鏈表*/
while (t != null) {
//獲取後繼
Node next = t.nextWaiter;
/*如果當前結點狀態不是等待狀態(Node.CONDITION),即取消等待了*/
if (t.waitStatus != Node.CONDITION) {
//當前結點的後繼引用置空
t.nextWaiter = null;
//trail爲null,出現這種情況,只能是第一次循環時,就發現頭結點取消等待了
// 則頭結點指向next
if (trail == null)
firstWaiter = next;
//否則trail的後繼指向next
else
trail.nextWaiter = next;
//如果next爲null,說明到了尾部,則lastWaiter指向上一個非取消(Node.CONDITION)結點的引用,該結點就是尾結點
if (next == null)
lastWaiter = trail;
}
/*否則,trail指向t*/
else
trail = t;
//t指向next,相當於遍歷鏈表了
t = next;
}
}
7.3.1.3 fullyRelease釋放所有重入鎖
await中鎖的釋放都是獨佔式的。由於可能是可重入鎖,因此fullyRelease方法會將當前獲取鎖的線程的全部重入鎖都一次性釋放掉。例如某個線程的鎖重入了一次,此時state變成2,在await中會一次性將2變成0。
我們常說說await方法必須要在獲取鎖之後調用,因爲在fullyRelease中會調用release獨佔式的釋放鎖,而release中調用了tryRelease方法,對於獨佔鎖的釋放,我們的實現會檢查是否是當前獲取鎖的線程,如果不是,那麼會拋出IllegalMonitorStateException異常。
fullyRelease大概步驟如下:
- 獲取當前的同步狀態state的值savedState,調用release釋放全部鎖包括重入的;
- 釋放成功那麼返回savedState,如果不是當前獲取鎖的線程那麼會拋出異常。
- 釋放失敗同樣也會拋出IllegalMonitorStateException異常。
- finally中,釋放成功什麼也不做;釋放失敗則將新添加進條件隊列的結點狀態設置爲Node.CANCELLED,即算一種非等待狀態。
/**
* 位於AQS中的方法,在結點被添加到條件隊列中之後調用
* 嘗試釋放當前線程所佔用的所有的鎖,並返回當前的鎖的同步狀態state
*
* @param node 剛添加的結點
* @return 當前的同步狀態
*/
final int fullyRelease(Node node) {
boolean failed = true;
try {
//獲取此時state值,
int savedState = getState();
//因爲state可能表鎖重入次數
//這裏調用release釋放鎖,參數爲此時state的值,表示釋放全部鎖,就算是重入鎖也一次性釋放完畢
//我們常說說await方法必須要在獲取鎖之後調用,就是這個方法的邏輯實現的:
//這個方法中調用了tryRelease方法,對於獨佔鎖我們的實現會檢查是否是當前獲取鎖的線程,如果不是,那麼會拋出IllegalMonitorStateException異常
//release釋放之後還會喚醒同步隊列的一個非取消的結點
if (release(savedState)) {
//釋放完成之後
failed = false;
//返回釋放之前的state同步狀態
return savedState;
}
/*如果釋放失敗,那麼也直接拋出異常*/
else {
throw new IllegalMonitorStateException();
}
} finally {
//如果釋放失敗,可能是拋出異常,那麼新加入結點的狀態改成Node.CANCELLED,即算一種非等待狀態
if (failed)
node.waitStatus = Node.CANCELLED;
}
}
7.3.1.4 isOnSyncQueue結點是否在同步隊列中
isOnSyncQueue用於檢測結點是否在同步隊列中,如果await等待的線程被喚醒/中斷,那麼對應的結點會被轉移到同步隊列之中!
/**
* 位於AQS中的方法,在fullyRelease之後調用
* 判斷結點是否在同步隊列之中
*
* @param node 新添加的結點
* @return 如果在返回true,否則返回false
*/
final boolean isOnSyncQueue(Node node) {
//如果狀態爲Node.CONDITION那一定是不在同步隊列中
//或者如果前驅爲null,表示肯定不在同步隊列中,因爲加入隊列的第一步就是設置前驅
if (node.waitStatus == Node.CONDITION || node.prev == null)
return false;
//如果next不爲null,說明肯定在同步隊列中了,而且還不是在尾部
if (node.next != null)
return true;
/*
有可能狀態不爲Node.CONDITION並且node.prev的值不爲null,此時還沒徹底添加到隊列中,但是不知道後續會不會添加成功,因爲enq入隊時CAS變更的tail可能失敗。
此時需要調用findNodeFromTail從後向前遍歷整個同步隊列,查找是否有該結點
*/
return findNodeFromTail(node);
}
/**
* 從尾部開始向前遍歷同步隊列,查找是否存在指定結點
*
* @param node 指定結點
* @return 如果存在,返回true
*/
private boolean findNodeFromTail(Node node) {
//遍歷同步隊列,查找是否存在該結點
Node t = tail;
for (; ; ) {
if (t == node)
return true;
if (t == null)
return false;
t = t.prev;
}
}
7.3.1.5 checkInterruptWhileWaiting檢測中斷以及被喚醒原因
await將線程中斷的狀態根據在不同情況下的中斷分成三種模式,除了下面的兩種之外,還使用0表示沒有中斷。在await方法的最後,會根據不同的中斷模式做出不同的處理。
/**
* 中斷模式
* 退出await方法之前,需要設置中斷狀態,由於此時已經獲得了鎖,即相當於設置一個標誌位。
* 如果是在調用signal或者signalAll方法之後被中斷,會是這個模式
*/
private static final int REINTERRUPT = 1;
/**
* 中斷模式
* 退出await方法之前,需要拋出InterruptedException異常
*如果是在等待調用signal或者signalAll之前就被中斷了,會是這個模式
*/
private static final int THROW_IE = -1;
checkInterruptWhileWaiting方法在掛起的線程被喚醒之後調用,用於檢測被喚醒的原因,並返回中斷模式。大概步驟爲:
- 判斷此時線程的中斷狀態,如果是中斷狀態,那麼調用transferAfterCancelledWait方法判斷那是在什麼時候中斷的;否則,返回0,說明是調用signal或者signalAll方法之後被喚醒的,並且沒有中斷。
- transferAfterCancelledWait方法中,如果是因爲在調用signal或者signalAll之前就被中斷了,那麼會將該結點狀態設置爲0,並調用enq方法加入到同步隊列中,返回THROW_IE模式,表示在await方法的最後會拋出異常;否則那就是在調用signal或者signalAll方法之後被中斷的,那麼在等待結點成功加入同步隊列之後,返回REINTERRUPT模式,表示在await方法最後會重新設置中斷狀態。
/**
* Condition中的方法
* 檢查被喚醒線程的中斷狀態,返回中斷模式
*/
private int checkInterruptWhileWaiting(Node node) {
//檢查中斷狀態
return Thread.interrupted() ?
//如果是中斷狀態,那麼調用transferAfterCancelledWait方法判斷是在什麼時候被中斷的
//如果在調用signal或者signalAll之前被中斷,那麼返回THROW_IE,表示在await方法最後會拋出異常
// 否則返回REINTERRUPT,表示在await方法最後會重新設置中斷狀態
(transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) :
//如果是未中斷狀態,那麼返回0
0;
}
/**
* AQS中的方法
* 判斷是在什麼時候被中斷的
*
* @param node 指定結點
* @return 如果在signal或者signalAll之前被中斷,將返回true;否則返回false
*/
final boolean transferAfterCancelledWait(Node node) {
//我們需要知道:signal或者signalAll方法中會將結點狀態從Node.CONDITION設置爲0
//這裏再試一次,如果在這裏將指定node從Node.CONDITIONCAS設置爲0成功,那麼表示該結點肯定是在signal或者signalAll方法被調用之前 被中斷而喚醒的
if (compareAndSetWaitStatus(node, Node.CONDITION, 0)) {
/*即使是因爲這樣被喚醒,此時也需要手動將結點加入同步隊列尾部
注意此時並沒有將結點移除條件隊列,因此在await方法的最後,會有這樣的判斷:
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
實際上就是再判斷這種情況!
*/
enq(node);
//入隊成功,返回true
return true;
}
/*否則,表示是在signal或者signalAll被調用之後,又被設置了中斷狀態的
* signal或者signalAll的方法中會將結點添加到同步隊列中,這裏循環判斷到底在不在隊列中,
* 因爲或者signal或者signalAll方法可能還沒有執行完畢,這裏等它執行完畢,然後返回false
* 如果不等他執行完畢,在回到外面的await方法中時,可能會影響後續的重新獲取鎖acquireQueued方法的執行
* */
while (!isOnSyncQueue(node))
Thread.yield();
//確定被加入到了同步隊列,則返回false
return false;
}
7.3.1.6 reportInterruptAfterWait對中斷模式進行處理
在await方法的最後,如果中斷模式不爲0,那麼會對之前設置的中斷模式進行統一處理:
- 如果是THROW_IE模式,即在調用signal或者signalAll之前就被中斷了,那麼拋出InterruptedException異常。
- 如果是REINTERRUPT模式,即在調用signal或者signalAll方法之後被中斷,那麼簡單的設置一箇中斷狀態爲true。
/**
* 中斷模式的處理:
* 如果是THROW_IE模式,那麼拋出InterruptedException異常
* 如果是REINTERRUPT模式,那麼簡單的設置一箇中斷狀態結尾true
*/
private void reportInterruptAfterWait(int interruptMode)
throws InterruptedException {
if (interruptMode == THROW_IE)
throw new InterruptedException();
else if (interruptMode == REINTERRUPT)
selfInterrupt();
}
7.3.2 await(time, TimeUnit)超時等待一段時間
當前線程進入等待狀態直到被通知、中斷,或者超時。
如果在超時時間範圍之內被喚醒了,則返回true;否則返回false。
該方法響應中斷。最終,如果該方法能夠返回,那麼該線程一定是又一次重新獲取到鎖了。
/**
* 超時等待
*
* @param time 時長
* @param unit 時長單位
* @return 如果在超時時間範圍之內被喚醒了,則返回true;否則返回false
* @throws InterruptedException 如果一開始就是中斷狀態或者如果在signal()或signalALL()方法調用之前就因爲中斷而被喚醒,那麼拋出異常
*/
public final boolean await(long time, TimeUnit unit)
throws InterruptedException {
/*內部結構和await方法差不多*/
//將超時時間轉換爲納秒
long nanosTimeout = unit.toNanos(time);
if (Thread.interrupted())
throw new InterruptedException();
Node node = addConditionWaiter();
int savedState = fullyRelease(node);
//獲取最大超時時間對應的時鐘納秒值
final long deadline = System.nanoTime() + nanosTimeout;
//超時標誌位timedout
boolean timedout = false;
int interruptMode = 0;
while (!isOnSyncQueue(node)) {
//超時時間如果小於等於0,表示已經超時了,還沒有返回,此時主動將結點加入同步隊列。
if (nanosTimeout <= 0L) {
//直接調用transferAfterCancelledWait,這裏的transferAfterCancelledWait方法被用來將沒有加入隊列的結點直接加入隊列,或者等待結點完成入隊
//該方法中,如果檢測到該結點此時(已超時)還沒被加入同步隊列,則手動添加並返回true;否則,等待入隊完成並返回false
timedout = transferAfterCancelledWait(node);
//結束循環
break;
}
//如果超時時間大於1000,則parkNanos等待指定時間,一段之間之後將會自動被喚醒
if (nanosTimeout >= spinForTimeoutThreshold)
LockSupport.parkNanos(this, nanosTimeout);
//檢查並設置中斷模式
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
//更新剩餘超時時間
nanosTimeout = deadline - System.nanoTime();
}
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
if (node.nextWaiter != null)
unlinkCancelledWaiters();
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
//注意:最終返回的是!timedout
// 即如果在parkNanos時間範圍之內被喚醒了,則返回true,否則如果是自然喚醒的,則返回false
return !timedout;
}
7.3.3 awaitUntil(deadline)超時等待時間點
與await(time, TimeUnit)的行爲一致,不同之處在於該方法的參數不是一段時間,而是一個時間點。
如果在超時時間點之前被喚醒了,則返回true;否則返回false。
該方法響應中斷。最終,如果該方法能夠返回,那麼該線程一定是又一次重新獲取到鎖了。
/**
* 超時等待指定時間點
*
* @param deadline 超時時間點
* @return 如果在超時時間點之前被喚醒了,則返回true;否則返回false
* @throws InterruptedException 如果一開始就是中斷狀態或者如果在signal()或signalALL()方法調用之前就因爲中斷而被喚醒,那麼拋出異常
*/
public final boolean awaitUntil(Date deadline)
throws InterruptedException {
//獲取指定時間點的毫秒
long abstime = deadline.getTime();
if (Thread.interrupted())
throw new InterruptedException();
Node node = addConditionWaiter();
int savedState = fullyRelease(node);
//超時標誌位timedout
boolean timedout = false;
int interruptMode = 0;
while (!isOnSyncQueue(node)) {
//如果當前時間毫秒 大於 指定時間點的毫秒,表明已經超時了,還沒有返回,此時主動將結點加入同步隊列。
if (System.currentTimeMillis() > abstime) {
//直接調用transferAfterCancelledWait,這裏的transferAfterCancelledWait方法被用來將沒有加入隊列的結點直接加入隊列,或者等待結點完成入隊
//該方法中,如果檢測到該結點此時(已超時)還沒被加入同步隊列,則手動添加並返回true;否則,等待入隊完成並返回false
timedout = transferAfterCancelledWait(node);
break;
}
//parkUntil方法使線程睡眠到指定時間點
LockSupport.parkUntil(this, abstime);
//檢查並設置中斷模式
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
if (node.nextWaiter != null)
unlinkCancelledWaiters();
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
//注意:最終返回的是!timedout
// 即如果在指定時間點之前被喚醒了,則返回true,否則返回false
return !timedout;
}
7.3.4 awaitNanos(nanosTimeout) 超時等待納秒
當前線程進入等待狀態直到被通知,中斷,或者超時。
返回(超時時間 - 實際返回所用時間)。如果返回值是0或者負數,那麼可以認定已經超時了。
該方法響應中斷。最終,如果該方法能夠返回,那麼該線程一定是又一次重新獲取到鎖了。
/**
* 超時等待指定納秒,若指定時間內返回,則返回 nanosTimeout-已經等待的時間;
*
* @param nanosTimeout 超時時間,納秒
* @return 返回 超時時間 - 實際返回所用時間
* @throws InterruptedException 如果一開始就是中斷狀態或者如果在signal()或signalALL()方法調用之前就因爲中斷而被喚醒,那麼拋出異常
*/
public final long awaitNanos(long nanosTimeout)
throws InterruptedException {
/*內部結構和await方法差不多*/
if (Thread.interrupted())
throw new InterruptedException();
Node node = addConditionWaiter();
int savedState = fullyRelease(node);
//獲取最大超時時間對應的時鐘納秒值
final long deadline = System.nanoTime() + nanosTimeout;
int interruptMode = 0;
while (!isOnSyncQueue(node)) {
//超時時間如果小於等於0,表示已經超時了,還沒有返回,此時主動將結點加入同步隊列。
if (nanosTimeout <= 0L) {
//直接調用transferAfterCancelledWait,這裏的transferAfterCancelledWait方法被用來將沒有加入隊列的結點直接加入隊列,或者等待結點完成入隊
//這裏不需要返回值
transferAfterCancelledWait(node);
break;
}
//如果超時時間大於1000,則parkNanos等待指定時間,一段之間之後將會自動被喚醒
if (nanosTimeout >= spinForTimeoutThreshold)
LockSupport.parkNanos(this, nanosTimeout);
//檢查並設置中斷模式
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
//更新剩餘超時時間
nanosTimeout = deadline - System.nanoTime();
}
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
if (node.nextWaiter != null)
unlinkCancelledWaiters();
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
//返回 最大超時時間對應的時鐘納秒值 減去 當前時間的納秒值
//可以想象,如果在指定時間之內返回,那麼將是正數,否則是0或負數
return deadline - System.nanoTime();
}
7.3.5 awaitUninterruptibly()不響應中斷等待
當前線程進入等待狀態直到被通知,不響應中斷。
其它線程調用該條件對象的signal()或signalALL()方法喚醒等待的線程之後,該線程纔可能從awaitUninterruptibly方法中返回。等待過程中如果當前線程被中斷,該方法仍然會繼續等待,同時保留該線程的中斷狀態。
最終,如果該方法能夠返回,那麼該線程一定是又一次重新獲取到鎖了。
/**
* 當前線程進入等待狀態直到被通知(signal或者signalAll)而喚醒,不響應中斷。
*/
public final void awaitUninterruptibly() {
/*內部結構和await方法差不多,比await更加簡單,因爲不需要響應中斷*/
Node node = addConditionWaiter();
int savedState = fullyRelease(node);
boolean interrupted = false;
/*循環,只有其他線程調用對應Condition的signal或者signalAll方法,將該結點加入到同步隊列中時,纔會停止循環*/
while (!isOnSyncQueue(node)) {
//直接等待
LockSupport.park(this);
//被喚醒之後,會判斷如果被中斷了,那麼記錄中斷標誌位,然後繼續循環
//在下一次的循環條件中會判斷是否被加入了同步隊列
if (Thread.interrupted())
//
interrupted = true;
}
//如果獲取鎖的等待途中被中斷過,或者前面的中斷標誌位爲true
if (acquireQueued(node, savedState) || interrupted)
//安全中斷,實際上就是將中斷標誌位改爲true,記錄一下而已
selfInterrupt();
}
7.4 通知機制原理
上面講了等待機制,並且提起了signal和signalAll方法將會喚醒等待的線程,這就是Condition的通知機制,相比於等待機制,通知機制還是比較簡單的!
7.4.1 signal通知單個線程
**signal方法首先進行了isHeldExclusively檢查,也就是當前線程必須是獲取了鎖的線程,否則拋出異常。接着將會嘗試喚醒在條件隊列中等待時間最長的結點,將其移動到同步隊列並使用LockSupport喚醒結點中的線程。大概步驟如下:
- 檢查調用signal方法的線程是否是持有鎖的線程,如果不是則直接拋出IllegalMonitorStateException異常。
- 調用doSignal方法將等待時間最長的一個結點從條件隊列轉移至同步隊列尾部,然後根據條件可能會嘗試喚醒該結點對應的線程。
/**
* Conditon中的方法
* 將等待時間最長的結點移動到同步隊列,然後unpark喚醒
*
* @throws IllegalMonitorStateException 如果當前調用線程不是獲取鎖的線程,則拋出異常
*/
public final void signal() {
/*1 首先調用isHeldExclusively檢查當前調用線程是否是持有鎖的線程
* isHeldExclusively方法需要我們重寫
* */
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
//獲取頭結點
Node first = firstWaiter;
/*2 如果不爲null,調用doSignal方法將等待時間最長的一個結點從條件隊列轉移至同步隊列尾部,然後根據條件可能會嘗試喚醒該結點對應的線程。*/
if (first != null)
doSignal(first);
}
/**
* AQS中的方法
* 檢測當前線程是否是持有獨佔鎖的線程,該方法AQS沒有提供實現(拋出UnsupportedOperationException異常)
* 通常需要我們自己重寫,一般重寫如下!
*
* @return true 是;false 否
*/
protected final boolean isHeldExclusively() {
//比較獲取鎖的線程和當前線程
return getExclusiveOwnerThread() == Thread.currentThread();
}
7.4.1.1 doSignal移除-轉移等待時間最長的結點
doSignal方法將在do while中從頭結點開始向後遍歷整個條件隊列,從條件隊列中移除等待時間最長的結點,並將其加入到同步隊列,在此期間會清理一些遍歷時遇到的已經取消等待的結點。
/**
* Conditon中的方法
* 從頭結點開始向後遍歷,從條件隊列中移除等待時間最長的結點,並將其加入到同步隊列
* 在此期間會清理一些遍歷時遇到的已經取消等待的結點。
*
* @param first 條件隊列頭結點
*/
private void doSignal(Node first) {
/*從頭結點開始向後遍歷,喚醒等待時間最長的結點,並清理一些已經取消等待的結點*/
do {
//firstWaiter指向first的後繼結點,並且如果爲null,則lastWaiter也置爲null,表示條件隊列沒有了結點
if ((firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
//first的後繼引用置空,這樣就將first出隊列了
first.nextWaiter = null;
/*循環條件
* 1 調用transferForSignal轉移結點,如果轉移失敗(結點已經取消等待了);
* 2 則將first賦值爲它的後繼,並且如果不爲null;
* 滿足上面兩個條件,則繼續循環
* */
} while (!transferForSignal(first) &&
(first = firstWaiter) != null);
}
7.4.1.1.1 transferForSignal轉移結點
transferForSignal會嘗試將遍歷到的結點轉移至同步隊列中,調用該方法之前並沒有顯示的判斷結點是不是處於等待狀態,而是在該方法中通過CAS的結果來判斷。
大概步驟爲:
- 嘗試CAS將結點等待狀態從Node.CONDITION更新爲0。這裏不存在併發的情況,因爲調用線程此時已經獲取了獨佔鎖,因此如果更改等待狀態失敗,那說明該結點原本就不是Node.CONDITION狀態,表示結點早已經取消等待了,則直接返回false,表示轉移失敗。
- CAS成功,則表示該結點是出於等待狀態,那麼調用enq將結點添加到同步隊列尾部,返回添加結點在同步隊列中的前驅結點。
- 獲取前驅結點的狀態ws。如果ws大於0,則表示前驅已經被取消了或者將ws改爲Node.SIGNAL失敗,表示前驅可能在此期間被取消了,那愛麼調用unpark方法喚醒被轉移結點中的線程,好讓它從await中的等待中醒來;否則,那就由它的前驅結點在獲取鎖之後釋放鎖時再喚醒。返回true。
/**
* 將結點從條件隊列轉移到同步隊列,並嘗試喚醒
*
* @param node 被轉移的結點
* @return 如果成功轉移,返回true;失敗則返回false
*/
final boolean transferForSignal(Node node) {
/*1 嘗試將結點的等待狀態變成0,表示取消等待
如果更改等待狀態失敗,那說明一定是原本就不是Node.CONDITION狀態,表示結點早已經取消等待了,則返回false。
這裏不存在併發的情況,因爲調用線程此時已經獲取了獨佔鎖*/
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false;
/*2 將結點添加到同步隊列尾部,返回添加結點的前驅結點*/
Node p = enq(node);
//獲取前驅結點的狀態ws
int ws = p.waitStatus;
/*3 如果ws大於0 表示前驅已經被取消了 或者 將ws改爲Node.SIGNAL失敗,表示前驅可能在此期間被取消了
則調用unpark方法喚醒被轉移結點中的線程,好讓它從await中的等待喚醒(後續嘗試獲取鎖)
否則,那就由它的前驅結點獲取鎖之後釋放鎖時再喚醒。
*/
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
LockSupport.unpark(node.thread);
//返回true
return true;
}
被喚醒後的線程(無論是在signal中被喚醒的、還是由於同步隊列前驅釋放鎖被喚醒的、還是由於在等待時因爲中斷而被喚醒的),都將從await()方法中的while循環中退出,下一步將調用同步器的acquireQueued()方法加入到獲取鎖的競爭中。
7.4.2 signalAll通知全部線程
signalAll方法,相當於對等待隊列中的每個結點均執行一次signal方法,效果就是將等待隊列中所有結點全部移動到同步隊列中,並嘗試喚醒每個結點的線程,讓他們競爭鎖。大概步驟爲:
- 檢查調用signalAll方法的線程是否是持有鎖的線程,如果不是則直接拋出IllegalMonitorStateException異常。
- 調用doSignalAll方法將條件隊列中的所有等待狀態的結點轉移至同步隊列尾部,然後根據條件可能會嘗試喚醒該結點對應的線程,相當於清空了條件隊列。
/**
* Conditon中的方法
* 將次Condition中的所有等待狀態的結點 從條件隊列移動到同步隊列中。
*
* @throws IllegalMonitorStateException 如果當前調用線程不是獲取鎖的線程,則拋出異常
*/
public final void signalAll() {
/*1 首先調用isHeldExclusively檢查當前調用線程是否是持有鎖的線程
* isHeldExclusively方法需要我們重寫
* */
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
//獲取頭結點
Node first = firstWaiter;
/*2 如果不爲null,調用doSignalAll方法將條件隊列中的所有等待狀態的結點轉移至同步隊列尾部,
然後根據條件可能會嘗試喚醒該結點對應的線程,相當於清空了條件隊列。*/
if (first != null)
doSignalAll(first);
}
7.4.2.1 doSignalAll移除-轉移全部結點
移除並嘗試轉移條件隊列的所有結點,實際上會將條件隊列清空。對每個結點調用transferForSignal方法。
/**
* Conditon中的方法
* 移除並嘗試轉移條件隊列的所有結點,實際上會將條件隊列清空
*
* @param first 條件隊列頭結點
*/
private void doSignalAll(Node first) {
//頭結點尾結點都指向null
lastWaiter = firstWaiter = null;
/*do while 循環轉移結點*/
do {
//next保存當前結點的後繼
Node next = first.nextWaiter;
//當前結點的後繼引用置空
first.nextWaiter = null;
//調用transferForSignal嘗試轉移結點,就算失敗也沒關係,因爲transferForSignal一定會對所有的結點都嘗試轉移
//可以看出來,這裏的轉移是一個一個的轉移的
transferForSignal(first);
//first指向後繼
first = next;
} while (first != null);
}
7.5 Condition的應用
有了Condition,,在配合Lock就能實現可控制的多線程案例,讓多線程按照我們業務需求去執行,比如下面的常見案例!
7.5.1 生產消費案例
使用Lock和Condition實現簡單的生產消費案例,生產一個產品就需要消費一個產品,一次最多隻能生產、消費一個產品
常見實現是:如果存在商品,那麼生產者等待,並喚醒消費者;如果沒有商品,那麼消費者等待,並喚醒生產者!
public class ProducerAndConsumer {
public static void main(String[] args) {
Resource resource = new Resource();
Producer producer = new Producer(resource);
Consumer consumer = new Consumer(resource);
//使用線程池管理線程
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(4, 4, 0, TimeUnit.SECONDS, new LinkedBlockingQueue<>(), Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());
//兩個生產者兩個消費者
threadPoolExecutor.execute(producer);
threadPoolExecutor.execute(consumer);
threadPoolExecutor.execute(producer);
threadPoolExecutor.execute(consumer);
threadPoolExecutor.shutdown();
}
/**
* 產品資源
*/
static class Resource {
private String name;
private int count;
//標誌位
boolean flag;
//獲取lock鎖,lock鎖的獲取和釋放需要代碼手動操作
ReentrantLock lock = new ReentrantLock();
//從lock鎖獲取一個condition,用於生產者線程在此等待和喚醒
Condition producer = lock.newCondition();
//從lock鎖獲取一個condition,用於消費者線程在此等待和喚醒
Condition consumer = lock.newCondition();
void set(String name) {
//獲得鎖
lock.lock();
try {
while (flag) {
try {
System.out.println("有產品了--" + Thread.currentThread().getName() + "生產等待");
//該生產者線程,在producer上等待
producer.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
++count;
this.name = name;
System.out.println(Thread.currentThread().getName() + "生產了" + this.name + +count);
flag = !flag;
//喚醒在consumer上等待的消費者線程,這樣不會喚醒等待的生產者
consumer.signalAll();
} finally {
//釋放鎖
lock.unlock();
}
}
void get() {
lock.lock();
try {
while (!flag) {
try {
System.out.println("沒產品了--" + Thread.currentThread().getName() + "消費等待");
//該消費者線程,在consumer上等待
consumer.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + "消費了" + this.name + count);
flag = !flag;
//喚醒在producer監視器上等待的生產者線程,這樣不會喚醒等待的消費者
producer.signalAll();
} finally {
lock.unlock();
}
}
}
/**
* 消費者行爲
*/
static class Consumer implements Runnable {
private Resource resource;
public Consumer(Resource resource) {
this.resource = resource;
}
@Override
public void run() {
while (true) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
//調用消費方法
resource.get();
}
}
}
/**
* 生產者行爲
*/
static class Producer implements Runnable {
private Resource resource;
public Producer(Resource resource) {
this.resource = resource;
}
@Override
public void run() {
while (true) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
//調用生產方法
resource.set("麪包");
}
}
}
}
7.5.2 實現商品倉庫
使用Lock和Condition實現較複雜的的生產消費案例,實現一箇中間倉庫,產品被存儲在倉庫之中,可以連續生產、消費多個產品。
這個實現,可以說就是簡易版的消息隊列!
/**
* @author lx
*/
public class BoundedBuffer {
public static void main(String[] args) {
Resource resource = new Resource();
Producer producer = new Producer(resource);
Consumer consumer = new Consumer(resource);
//使用線程池管理線程
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(4, 4, 0, TimeUnit.SECONDS, new LinkedBlockingQueue<>(), Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());
//兩個生產者兩個消費者
threadPoolExecutor.execute(producer);
threadPoolExecutor.execute(consumer);
threadPoolExecutor.execute(producer);
threadPoolExecutor.execute(consumer);
threadPoolExecutor.shutdown();
}
/**
* 產品資源
*/
static class Resource {
// 獲得鎖對象
final Lock lock = new ReentrantLock();
// 獲得生產監視器
final Condition notFull = lock.newCondition();
// 獲得消費監視器
final Condition notEmpty = lock.newCondition();
// 定義一個數組,當作倉庫,用來存放商品
final Object[] items = new Object[100];
/*
* putpur:生產者使用的下標索引;
* takeptr:消費者下標索引;
* count:用計數器,記錄商品個數
*/
int putptr, takeptr, count;
/**
* 生產方法
* @param x
* @throws InterruptedException
*/
public void put(Object x) throws InterruptedException {
// 獲得鎖
lock.lock();
try {
// 如果商品個數等於數組的長度,商品滿了將生產將等待消費者消費
while (count == items.length) {
notFull.await();
}
// 生產索引對應的商品,放在倉庫中
Thread.sleep(50);
items[putptr] = x;
// 如果下標索引加一等於數組長度,將索引重置爲0,重新開始
if (++putptr == items.length) {
putptr = 0;
}
// 商品數加1
++count;
System.out.println(Thread.currentThread().getName() + "生產了" + x + "共有" + count + "個");
// 喚醒消費線程
notEmpty.signal();
} finally {
// 釋放鎖
lock.unlock();
}
}
/**
* 消費方法
* @return
* @throws InterruptedException
*/
public Object take() throws InterruptedException {
//獲得鎖
lock.lock();
try {
//如果商品個數爲0.消費等待
while (count == 0) {
notEmpty.await();
}
//獲得對應索引的商品,表示消費了
Thread.sleep(50);
Object x = items[takeptr];
//如果索引加一等於數組長度,表示取走了最後一個商品,消費完畢
if (++takeptr == items.length)
//消費索引歸零,重新開始消費
{
takeptr = 0;
}
//商品數減一
--count;
System.out.println(Thread.currentThread().getName() + "消費了" + x + "還剩" + count + "個");
//喚醒生產線程
notFull.signal();
//返回消費的商品
return x;
} finally {
//釋放鎖
lock.unlock();
}
}
}
/**
* 生產者行爲
*/
static class Producer implements Runnable {
private Resource resource;
public Producer(Resource resource) {
this.resource = resource;
}
@Override
public void run() {
while (true) {
try {
resource.put("麪包");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
/**
* 消費者行爲
*/
static class Consumer implements Runnable {
private Resource resource;
public Consumer(Resource resource) {
this.resource = resource;
}
@Override
public void run() {
while (true) {
try {
resource.take();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
7.5.3 輸出ABCABC……
編寫一個程序,開啓3 個線程,這三個線程的name分別爲A、B、C,每個線程將自己的名字 在屏幕上打印10 遍,要求輸出的結果必須按名稱順序顯示,例如:ABCABCABC…
這個案例中,我們需要手動控制相關線程在指定線程之前執行。
/**
* @author lx
*/
public class PrintABC {
ReentrantLock lock = new ReentrantLock();
Condition A = lock.newCondition();
Condition B = lock.newCondition();
Condition C = lock.newCondition();
/**
* flag標誌,用於輔助控制順序,默認爲1
*/
private int flag = 1;
public void printA(int i) {
lock.lock();
try {
while (flag != 1) {
try {
A.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + " " + i);
flag = 2;
B.signal();
} finally {
lock.unlock();
}
}
public void printB(int i) {
lock.lock();
try {
while (flag != 2) {
try {
B.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + " " + i);
flag = 3;
C.signal();
} finally {
lock.unlock();
}
}
public void printC(int i) {
lock.lock();
try {
while (flag != 3) {
try {
C.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + " " + i);
System.out.println("---------------------");
flag = 1;
A.signal();
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
PrintABC testABC = new PrintABC();
Thread A = new Thread(new A(testABC), "A");
Thread B = new Thread(new B(testABC), "B");
Thread C = new Thread(new C(testABC), "C");
A.start();
B.start();
C.start();
}
static class A implements Runnable {
private PrintABC testABC;
public A(PrintABC testABC) {
this.testABC = testABC;
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
testABC.printA(i + 1);
}
}
}
static class B implements Runnable {
private PrintABC testABC;
public B(PrintABC testABC) {
this.testABC = testABC;
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
testABC.printB(i + 1);
}
}
}
static class C implements Runnable {
private PrintABC testABC;
public C(PrintABC testABC) {
this.testABC = testABC;
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
testABC.printC(i + 1);
}
}
}
}
8 總結
AQS是JUC中實現同步組件的基礎框架,有了AQS我們自己也能比較輕鬆的實現自定義的同步組件。
AQS中提供了同步隊列的實現,用於實現鎖的獲取和釋放,沒有獲取到鎖的線程將進入同步隊列排隊等待,是實現線程同步的基礎。
AQS內部還提供了條件隊列的實現,條件隊列用於實現線程之間的主動等待、喚醒機制,是實現線程 有序可控 同步的不可缺少的部分。
一個鎖對應一個同步隊列,對應多個條件變量,每個條件變量有自己的一個條件隊列,這樣就可以實現按照業務需求讓不同的線程在不同的條件隊列上等待,相對於Synchronized的只有一個條件隊列,功能更加強大!
最後,當我們深入源碼時,發現對於最基礎的同步支持,比如可見性、原子性、線程等待、喚醒等操作,AQS也是調用的其他工具、或者利用率其他特性:
- 同步狀態state被設置爲volatile類型,這樣在獲取、更新時保證了可見性,還可以禁止重排序!
- 使用CAS來更新變量,來保證單個變量的複合操作(讀-寫)具有原子性!而CAS方法內部又調用了unsafe的方法。這個Unsafe類,實際上是比AQS更加底層的底層框架,或者可以認爲是AQS框架的基石。
- CAS操作在Java中的最底層的實現就是Unsafe類提供的,它是作爲Java語言與Hospot源碼(C++)以及底層操作系統溝通的橋樑,可以去了解Unsafe的一些操作。
- 對於線程等待、喚醒,是調用了LockSupport的park、unpark方法,如果去看LockSupport的源碼,那麼實際上最終還是調用Unsafe類中的方法!
學習了AQS,對於我們後續將會進行的Lock鎖等JUC同步組件的實現分析將會大有幫助!
如果有什麼不懂或者需要交流,可以留言。另外希望點贊、收藏、關注,我將不間斷更新各種Java學習博客!