本篇介紹Lock接口、重入鎖、讀寫鎖和Condition等,部門內容總結摘抄自《Java併發編程的藝術》和《Java併發編程實戰》,僅作筆記。
Lock
鎖是用來控制多個線程方文共享資源的方式,一般來說,一個鎖能夠防止多個線程同時訪問共享資源。在Lock接口出現之前,Java程序是靠synchronized關鍵字實現鎖功能的,在JAVA SE5之後,併發包中新增了Lock接口以及相關實現類用來實現鎖功能。它提供了與synchronized關鍵字類似的同步功能,只是在使用時需要顯示的獲取和釋放鎖。雖然它缺少了隱式獲取釋放鎖的便捷性,但卻擁有了鎖獲取與釋放的可操作性、可中斷的獲取鎖以及超時獲取鎖等多種synchronized關鍵字所不具備的同步特性。
API
Lock接口包含以下方法:
//獲得鎖
void lock();
//如果當前線程未被中斷,則獲取鎖,可響應中斷
void lockInterruptibly();
//獲取一個綁定到此Lock實例的Condition對象
Condition newCondition();
//在調用時鎖爲空閒狀態才獲取鎖,可響應中斷
boolean tryLock();
//如果鎖在給定的時間內空閒,且未被中斷,則獲取鎖
boolean tryLock(long time, TimeUnit unit);
//釋放鎖
void unlock();
Lock的使用方式如下:
Lock lock = new Lock實現類();
lock.lock();
try{
}finally{
lock.unlock();
}
在finally塊中釋放鎖,目的是保證在獲取到鎖後,最終能被釋放。
Lock接口提供的synchronized關鍵字所不具備的主要特性如下表。
特性 | 描述 |
---|---|
嘗試非阻塞的獲取鎖 | 當前線程嘗試獲取鎖,如果這一時刻沒有鎖沒有被其他線程獲取到,則成功獲取並持有鎖 |
能被中斷的獲取鎖 | 與synchronized不同,獲取到鎖的線程能夠響應中斷,當獲取到鎖的線程被中斷時,中斷異常將會被拋出,鎖也會被釋放 |
超時獲取鎖 | 在指定的截止時間之前獲取鎖,如果截止時間到了仍舊無法獲取鎖,則返回 |
重入鎖ReentrantLock
重入鎖ReentrantLock,實現了Lock接口,並且提供了與synchronized相同的互斥性和內存可見性。在獲取ReentrantLock時,有着與進入同步代碼塊相同的內存語義,在釋放ReentrantLock時,有着與退出同步代碼塊相同的內存語義。與synchronized一樣,ReentrantLock還提供可重入的加鎖語義。
ReentrantLock支持在Lock接口中定義的所有獲取鎖模式,並且與synchronized相比,它還支持獲取鎖時的公平和非公平性選擇。如果在絕對時間上,先對鎖進行獲取的請求一定先被滿足,那麼這個鎖是公平的,反之,是不公平的。公平的獲取鎖,也就是等待時間最常的線程最優先獲取鎖,也可以說鎖獲取時順序地。
API
ReentrantLock的構造函數如下:
//默認構造函數
ReentrantLock();
//傳入是否爲公平鎖的參數的構造函數
ReentrantLock(boolean fair);
常用方法如下:
//返回當前鎖是否是公平鎖
public final boolean isFair();
//返回此鎖是否被任何線程持有
final boolean isLocked();
//獲取鎖,獲取不到邊一直等待
final boolean isLocked();
//獲得與此lock對象綁定的Condition對象
public Condition newCondition();
//嘗試獲取鎖,無論成功還是失敗都直接返回
public boolean tryLock();
//在指定的時間內獲取鎖,超時返回
public boolean tryLock(long timeout, TimeUnit unit);
//釋放鎖
public void unlock();
公平性
在ReentrantLock的構造函數中提供了兩種公平性選擇:創建一個非公平的鎖或者創建一個公平的鎖。在公平的鎖上,鎖的獲取順序與請求的絕對時間順序一致,如果有另一個線程持有這個鎖或有其他線程在隊列中等待這個鎖,新發出請求的線程將被放入隊列末尾。在非公平的鎖上,如果一個線程發出請求的同時該鎖的狀態變爲可用,那麼這個線程將跳過隊列中所有等待的線程並獲得這個鎖,只有當該鎖被某個線程持有時,新發出請求的線程纔會被放入隊列。默認的ReentrantLock是非公平的。
在激烈競爭的情況下,非公平鎖的性能高於公平鎖的性能,其中一個原因在於:在恢復一個被掛起的線程與該線程真正開始運作之間存在着嚴重的延遲。假設線程A持有一個鎖,並且線程B請求這個鎖。由於這個鎖已被線程A持有,因此B將被掛起。當A釋放鎖時,B將被喚醒,因此再次嘗試獲取鎖。與此同時,C也請求這個鎖,而C有可能在B被完全喚醒之前獲得、使用以及釋放這個鎖,這樣的情況是一種雙贏的局面。
當持有鎖的時間相對較長,或者請求鎖的平均時間間隔較長,應該使用公平鎖。在這些情況下,“插隊”帶來的吞吐量提升則可能不會出現。
實現分析
ReentrantLock類的功能基本上都是通過名爲Sync的抽象靜態內部類實現的,而Sync又繼承於AbstractQueuedSynchronizer類,關於AbstractQueuedSynchronized類在這篇文章中介紹了,此處不再贅述。獲取鎖的具體實現則由繼承了Sync類的NofairSync類和FairSync完成,這兩個類分別對應非公平鎖和公平鎖的實現。
以上四個類的類圖如下:
非公平鎖獲取鎖的lock()方法的具體實現如下:
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
在以上代碼中,先使用CAS判斷當前鎖是否被任何線程佔有(即state是否爲0),如果未佔有,則將state設置爲1,並且保存獲取鎖的線程;如果已有其他線程佔有該鎖(即state大於0),則調用AQS中的acquire()方法。這是一個模版方法,其基本邏輯是調用tryAcquire()方法(該方法由子類實現)嘗試獲取鎖,如果獲取失敗則構造一個節點對象(包含線程信息)並加入同步隊列的尾部。因此具體獲取鎖的代碼還是由NonfairSync以及FairSync類實現。
非公平鎖NonfairSync的tryAcquire()方法實際上是調用父類Sync的nonfairTryAcquire()方法,因此我們直接看這個方法,其代碼如下:
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
在以上代碼中,首先判斷該鎖是否未被任意線程獲取(state是否爲0),如果是,則使用CAS將state設置爲指定的值然後保存獲取鎖的線程,返回true;否則判斷當前線程是否爲已經獲取鎖的線程,如果是,則更新state的值,然後返回true(這一過程使得ReentrantLock可重入)。否則返回false,表示鎖獲取失敗。
公平鎖的lock()方法如下:
final void lock() {
acquire(1);
}
與非公平鎖不同,公平鎖的lock()方法並沒有立即嘗試獲取鎖,而是直接調用acquire()方法。原因與他們的區別有關,非公平鎖與請求時間順序無關,而公平鎖需要按照FIFO的順序,因此這裏不能直接獲取鎖。
公平鎖的tryAcquire()方法代碼如下:
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
以上代碼邏輯爲:當該鎖沒有被任何線程獲取時,首先判斷是否有線程在等待獲取,如果沒有則使用CAS設置state的值並且保存獲取鎖的線程。當該鎖已被任意線程獲取的流程與非公平鎖一致。
除了獲取鎖外,因爲可重入的特性,Sync還實現了釋放鎖tryRelease()方法,其代碼如下:
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
首先判斷當前線程是否是獲取鎖的線程,如果不是則拋出IllegalMonitorStateException。然後判斷同步狀態state是否爲0,如果爲0則表示線程釋放了該鎖,將保存的獲取鎖的線程置爲null。最後更新state的值。
選擇synchronized還是ReentrantLock?
ReentrantLock在加鎖和內存上提供的語義與內置鎖相同,此外它還提供了一些其他功能,包括定時的鎖等待、可中斷的鎖等待、公平性以及實現非塊結構的加鎖。如此看來應該使用ReentrantLock代替內置鎖,但事實並非如此。
內置鎖與顯式鎖相比,還是具有很大的優勢。內置鎖爲許多開發人員所熟悉,並且簡介緊湊。在許多現有的程序中已經使用了內置鎖,如果將這兩種機制混合使用,不僅容易令人困惑,也容易發生錯誤。ReentrantLock的危險性要比同步機制高,如果忘記在finally中調用unlock,雖然代碼表面上可以正常運行,實際上很可能會傷及其他代碼。僅當內置鎖不能滿足需求時,才考慮使用ReentrantLock。
讀寫鎖
ReentrantLock實現了一種標準的互斥鎖:每次最多隻有一個線程能持有ReentrantLock。但對於維護數據的完整性來說,互斥通常是一種過於強硬的加鎖規則,因此也就不必要的限制了併發性。互斥是一種保守的加鎖策略,雖然可以避免“寫/寫”衝突和“寫/讀”衝突,但同樣也避免了“讀/讀”衝突。許多情況下,數據機構上的訪問操作都是讀操作。此時,如果能夠放寬加鎖需求,允許多個執行讀操作的線程同時訪問數據結構,程序的性能就會得到提升。只要每個線程都能確保讀取到最新的數據,並且在讀數據時不會有其他線程修改數據,那就不會發生問題。一個資源可以被多個讀操作訪問,或者被一個寫操作訪問,但這兩者不能同時進行。
在Java 5以前,如果需要完成上述工作需要使用Java的等待通知機制,當寫操作開始時,所有晚於寫操作的線程都會進入等待狀態,只有寫操作完成並進行通知後,所有等待的讀操作才能繼續執行,這樣做的目的是使讀操作可以讀取到正確的數據,不會出現髒讀。
一般情況下,讀寫鎖的性能都比互斥鎖好,因爲大多數場景讀是多於寫的。
下面逐一介紹讀寫鎖相關的API。
ReadWriteLock接口
ReadWriteLock接口只有兩個方法,一個用於讀操作,一個用於寫操作。ReadWriteLock接口所有的實現類都必須保證一個線程獲取讀鎖後可以看到之前發佈的寫鎖的所有更新。
//獲取讀鎖
Lock readLock();
//獲取寫鎖
Lock writeLock();
ReentrantReadWriteLock類
ReentrantReadWriteLock類實現了ReadWriteLock接口,ReentrantReadWriteLock除了實現接口方法外,還提供了一些便於監控其內部工作狀態的方法。
ReentrantReadWriteLock的兩個構造函數實現如下:
//默認構造函數
public ReentrantReadWriteLock() {
this(false);
}
//傳入是否爲公平鎖的參數
public ReentrantReadWriteLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
readerLock = new ReadLock(this);
writerLock = new WriteLock(this);
}
其他常用方法如下:
//返回當前擁有寫鎖的線程,如果沒有返回null
final Thread getOwner();
//返回正在等待獲取讀鎖的線程集合
protected Collection<Thread> getQueuedReaderThreads();
//返回當前線程在此鎖重入讀鎖的數量
final int getReadHoldCount();
//返回當前線程在此鎖重入寫鎖的數量
getWriteHoldCount;
//返回當前線程是否爲公平鎖
public final boolean isFair();
//查詢寫鎖是否由任何線程持有
public boolean isWriteLocked();
//查詢寫鎖是否由當前線程持有
public boolean isWriteLockedByCurrentThread();
//返回讀鎖
public ReentrantReadWriteLock.ReadLock readLock();
//返回寫鎖
public ReentrantReadWriteLock.WriteLock writeLock();
實現分析
在第二個構造函數中初始化了一個ReadLock對象和WriteLock對象,這兩個對象都是ReentrantReadWriteLock的靜態內部類,而這兩個類又都依賴名爲Sync的一個抽象靜態內部類,這裏與ReentrantLock類似,Sync同樣繼承了AQS。這幾個類的類圖如下:
讀鎖ReadLock的lock()方法調用的是AQS中的獲取共享鎖的方法acquireShared(),unlock()方法調用的是AQS釋放共享鎖的方法releaseShared()。寫鎖WriteLock的lock()方法調用的是AQS中獲取獨佔鎖的方法acquire(),unlock()方法調用的是AQS釋放獨佔鎖的方法release()。與ReentrantLock一樣,真正實現獲取鎖的是sync中的tryAcquire()和tryAcquireShared(),釋放鎖的是tryRelease()和tryReleaseShared()。原因在上面介紹ReenrantLock類時介紹過了,此處不再贅述。因此我們只討論這四個方法。
在接上讀寫鎖的獲取與釋放之前我們還需要介紹一些讀寫狀態的設計。在ReentrantLock中自定義同步器的同步狀態表示鎖被一個線程重複獲取的次數,而讀寫鎖的自定義同步器需要在同步狀態上維護一個寫線程和多個讀線程的狀態。
在一個整型變量上維護多種狀態,就需要使用“按位切割”。讀寫鎖將變量切分成了兩個部分,高16位表示讀,低16位表示寫,劃分方式如下圖:
當前同步狀態表示一個線程已經獲取了寫鎖,且重入了兩次,同時也連續獲取了兩次讀鎖。假設當前同步狀態值爲S,寫狀態等於S&0x0000FFFF(將高16位全部抹去),讀狀態等於S>>>16(無符號補0右移16位)。當寫狀態增加1時,等於S+1,當讀狀態增加1時,等於S+(1<<<16),也就是S+0x00010000。ReentrantReadWriteLock中計算讀寫鎖都是類似這樣運算的。
根據狀態的劃分能得出一個結論:S不等於0時,當寫狀態(S&0x0000FFFF)等於0時,則讀狀態(S>>>16)大於0,即讀鎖已被獲取。
寫鎖是一個支持重進入的互斥鎖。如果當前線程已經獲取了寫鎖,則增加寫狀態。如果當前線程在獲取寫鎖時,讀鎖已被獲取(讀狀態不爲0)或者該線程不是已經獲取寫鎖的線程,則當前線程進入等待狀態。
獲取寫鎖的tryAcquire()方法代碼如下:
protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
int c = getState();
int w = exclusiveCount(c);
if (c != 0) {
//如果同步狀態不爲0且寫狀態爲0,則讀狀態不爲0,即讀鎖已被獲取
if (w == 0 || current != getExclusiveOwnerThread())
return false;
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
setState(c + acquires);
return true;
}
if (writerShouldBlock() || !compareAndSetState(c, c + acquires))
return false;
setExclusiveOwnerThread(current);
return true;
}
首先獲取寫狀態w,判斷同步狀態c不爲0時,寫狀態w爲0或者當前線程並非獲取寫鎖的線程,則返回false。然後判斷寫鎖重入次數是否達到上限,更新同步狀態等。
在以上代碼的邏輯中,如果存在讀鎖則寫鎖不能被獲取,原因在於:讀寫鎖要確保寫鎖的操作對讀鎖可見,如果允許讀鎖在已被獲取的情況下對寫鎖的獲取,那麼正在運行的其他線程就無法感知到當前寫線程的操作。因此,只有等其他讀線程都釋放了讀鎖,寫鎖才能被當前線程獲取,而寫鎖一旦被獲取,則其他讀寫線程的後續訪問均被阻塞。
寫鎖的釋放與ReentrantLock的釋放過程基本類似,每次釋放均減少寫狀態,當寫狀態爲0時表示寫鎖已被釋放,從而等待的讀寫線程能夠繼續訪問讀寫鎖,同時前次寫線程的修改對後續讀寫線程可見。
讀鎖是一個支持重進入的共享鎖,它能夠被多個線程同時獲取,在沒有其他寫線程訪問時,讀鎖總是能被成功獲取,所做的也只是增加讀狀態。如果當前線程已經獲取了讀鎖,則增加讀狀態。如果當前線程在獲取讀鎖時,寫鎖已被其他線程獲取,則進入等待狀態。讀狀態是所有線程獲取讀鎖次數的綜合,每個線程各自獲取讀鎖的次數只能選擇保存在ThreadLocal中,由線程自身維護,這使得獲取讀鎖的代碼變得複雜。
獲取讀鎖的tryAcquireShared()方法的代碼如下:
protected final int tryAcquireShared(int unused) {
Thread current = Thread.currentThread();
int c = getState();
if (exclusiveCount(c) != 0 && getExclusiveOwnerThread() != current)
return -1;
int r = sharedCount(c);
if (!readerShouldBlock() && r < MAX_COUNT && compareAndSetState(c, c + SHARED_UNIT)) {
if (r == 0) {
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {
firstReaderHoldCount++;
} else {
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
cachedHoldCounter = rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
rh.count++;
}
return 1;
}
return fullTryAcquireShared(current);
}
首先判斷是否有其他線程獲取了寫鎖,如果是,則當前線程獲取讀鎖失敗,進入等待狀態。否則當前線程增加讀狀態,成功獲取讀鎖。讀鎖的每次釋放均減少讀狀態,減少的值是(1<<16)。
鎖降級
鎖降級是指當前線程擁有寫鎖,再獲取到讀鎖,隨後釋放寫鎖的過程。鎖降級主要是爲了保證數據的可見性,如果當前線程不獲取讀鎖而是直接釋放寫鎖,假設此刻另一個線程獲取了寫鎖並修改了數據,那麼當前線程無法感知到另一個線程的數據更新。如果當前線程獲取讀鎖,即遵循鎖降級的步驟,則線程T將會被阻塞,直到當前線程使用數據並釋放讀鎖之後,線程T才能獲取寫鎖並進行數據更新。
筆者看到書中上面這一段內容懷疑了一下,因爲看似好像不使用鎖降級也是一樣的,因爲鎖的存在就保證了數據的可見性。看到網上很多大牛的理解,比較靠譜的大概如下:
鎖降級在一個讀寫操作都有的場景中,線程A執行完寫操作後釋放了寫鎖,線程B獲取寫鎖並修改了數據,隨後線程A獲取讀鎖然後執行後續操作。由於數據可見性,此時線程A確實讀到了線程B的執行結果,但線程A的場景可能需要使用本身寫操作的結果,這時程序可能就會出現意外。如果使用鎖降級就不存在這種情況,線程A獲取寫鎖,獲取讀鎖,釋放寫鎖,這時沒有其他線程可以獲取寫鎖修改數據。
Condition
任意一個Java對象都擁有一組監視器方法,主要包括wait()、wait(long timeout)、notify()以及notifyAll()方法,這些方法與synchronized同步關鍵字配合,可以實現等待/通知模式。Condition接口也提供了類似Object的監視器方法,與Lock配合可以實現等待/通知模式,但這兩者在使用方式以及功能特性上有一些差別,如下圖:
對比項 | Object Monitor Methods | Condition |
---|---|---|
前置條件 | 獲取對象的鎖 |
調用Lock.lock()獲取鎖 調用Lock.newCondition()獲取Condition對象 |
調用方式 | 直接調用,如object.wait() | 直接調用,如condition.await() |
等待隊列個數 | 一個 | 多個 |
當前線程釋放鎖並進入等待狀態 | 支持 | 支持 |
當前線程釋放鎖並進入等待狀態,在等待狀態中不響應中斷 | 不支持 | 支持 |
當前線程釋放鎖並進入超時等待狀態 | 支持 | 支持 |
當前線程釋放鎖並進入等待狀態到將來的某個時間 | 不支持 | 支持 |
喚醒等待隊列中的一個線程 | 支持 | 支持 |
喚醒等待隊列中的全部線程 | 支持 | 支持 |
Condition定義了等待/通知兩種類型的方法,當前線程調用這些方法時,需要提前獲取到Condition對象關聯的鎖。Condition對象是由Lock對象創建出來的,因此依賴Lock對象。
Condition的方法如下:
//當前線程進入等待狀態直到被通知或中斷
void await();
//當前線程進入等待狀態直到被通知,對中斷不敏感
void awaitUninterruptibly();
//當前線程進入等待狀態直到被通知、中斷或超時。返回值表示剩餘時間,如果返回值爲0或負數,則已超時
long awaitNanos(long nanosTimeout);
//當前線程進入等待狀態直到被通知、中斷或到某個時間。
//如果沒有到指定時間就被通知,方法返回true,否則返回false
boolean awaitUntil(Date deadline);
//喚醒一個等待在Condition上的線程,該線程從等待方法返回前必須獲得與Condition相關聯的鎖
void signal();
//喚醒所有等待在Condition上的線程,能夠從等待方法返回的線程必須獲得與Condition相關聯的鎖
void signalAll();
獲取一個Condition必須通過Lock的newCondition()方法,多次調用則可以獲取多個Condition對象,因此一個Lock可以有多條等待隊列。下面通過一個有界隊列的示例來深入瞭解Condition的使用方式,有界隊列是一種特殊的隊列,當隊列爲空時,隊列的獲取操作將會阻塞獲取線程,直到隊列中有新增元素,當隊列已滿時,隊列的插入操作將會阻塞插入線程,知道隊列出現“空位”。代碼如下所示:
public class ConditionBoundedQueue {
private int[] items = new int[5];
private int count,addIndex,removeIndex;
private Lock lock = new ReentrantLock();
private Condition notEmpty =lock.newCondition();
private Condition notFull = lock.newCondition();
public static void main(String[] args) {
ConditionBoundedQueue boundedQueue = new ConditionBoundedQueue();
for (int i=0;i<10;i++){
new Producer(boundedQueue).start();
}
for (int i=0;i<10;i++){
new Consumer(boundedQueue).start();
}
}
public static class Producer extends Thread{
private ConditionBoundedQueue boundedQueue;
Producer(ConditionBoundedQueue boundedQueue){
this.boundedQueue = boundedQueue;
}
@Override
public void run() {
boundedQueue.add(new Random().nextInt(100));
}
}
public static class Consumer extends Thread{
private ConditionBoundedQueue boundedQueue;
Consumer(ConditionBoundedQueue boundedQueue){
this.boundedQueue = boundedQueue;
}
@Override
public void run() {
boundedQueue.remove();
}
}
public void add(int item){
lock.lock();
try {
while (count == items.length) {
notFull.await();
}
items[addIndex] = item;
count++;
if (++addIndex == items.length) {
addIndex = 0;
}
System.out.println("增加一個值後數組爲:" + Arrays.toString(items));
notEmpty.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void remove(){
lock.lock();
try {
while (count == 0) {
notEmpty.await();
}
count--;
items[removeIndex] = 0;
if (++removeIndex == items.length) {
removeIndex = 0;
}
System.out.println("刪除一個值後數組爲:" + Arrays.toString(items));
notFull.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
輸出結果爲:
增加一個值後數組爲:[92, 0, 0, 0, 0]
增加一個值後數組爲:[92, 56, 0, 0, 0]
增加一個值後數組爲:[92, 56, 26, 0, 0]
增加一個值後數組爲:[92, 56, 26, 25, 0]
增加一個值後數組爲:[92, 56, 26, 25, 79]
刪除一個值後數組爲:[0, 56, 26, 25, 79]
增加一個值後數組爲:[48, 56, 26, 25, 79]
刪除一個值後數組爲:[48, 0, 26, 25, 79]
增加一個值後數組爲:[48, 75, 26, 25, 79]
刪除一個值後數組爲:[48, 75, 0, 25, 79]
刪除一個值後數組爲:[48, 75, 0, 0, 79]
刪除一個值後數組爲:[48, 75, 0, 0, 0]
增加一個值後數組爲:[48, 75, 45, 0, 0]
刪除一個值後數組爲:[0, 75, 45, 0, 0]
刪除一個值後數組爲:[0, 0, 45, 0, 0]
增加一個值後數組爲:[0, 0, 45, 17, 0]
刪除一個值後數組爲:[0, 0, 0, 17, 0]
刪除一個值後數組爲:[0, 0, 0, 0, 0]
增加一個值後數組爲:[0, 0, 0, 0, 52]
刪除一個值後數組爲:[0, 0, 0, 0, 0]
實現分析
Condition的實現類是ConditionObject,ConditionObject是同步器AQS的內部類,因爲Condition的操作需要獲取相關的鎖,所以作爲同步器的內部類也較爲合理。每個Condition對象都包含一個等待隊列,該隊列是Condition對象實現等待/通知功能的關鍵。
等待隊列是一個FIFO的隊列,在隊列的每個節點都包含了一個線程引用,該線程就是在Condition對象上等待的線程,如果一個線程調用了Condition.await()方法,那麼該線程就會釋放鎖、構造節點加入等待隊列並進入等待狀態。如果瞭解AQS就會發現這裏好像與加入同步隊列類似,事實上等待隊列的節點類就是AQS的節點類Node。
一個Condition包含一個等待隊列,之前提到過,一個Lock每調用一次newCondition()方法就有多個Condition。因此一個Lock擁有多個等待隊列,但只有一個同步隊列。
當線程調用await()方法時,當前線程(同步隊列的首節點)需要做的事情如下:構造節點,加入等待隊列中,釋放同步狀態,喚醒同步隊列中的後繼節點,進入等待狀態。
調用Condition的signal()方法,將會喚醒在等待隊列中等待時間最長的節點(首節點),在喚醒節點之前,會將節點移到同步隊列中。調用該方法的前置條件是當前線程必須獲取了鎖,然後獲取等待隊列的首節點,將其移動到同步隊列並使用LockSupport喚醒節點中的線程。
被喚醒後的節點,將從await()方法中的while循環中退出,進而調用同步器的acquireQueued()方法加入到獲取同步狀態的競爭中。成功獲取同步狀態後,被喚醒的線程將從先前調用的await()方法返回,此時該線程已經成功獲取了鎖。Condition的sigalAll()方法,相當於對等待隊列中的每個節點都執行一個signal()方法,效果就是將等待隊列中的所有節點全部移動到同步隊列中,並喚醒每個節點的線程。