標籤: 「我們都是小青蛙」公眾號文章
Condition
ReentrantLock的內部實現
看完了AQS
中的底層同步機制,我們來簡單分析一下之前介紹過的ReentrantLock
的實現原理。先回顧一下這個顯式鎖的典型使用方式:
Lock lock = new ReentrantLock();
lock.lock();
try {
加鎖後的程式碼
} finally {
lock.unlock();
}
複製程式碼
ReentrantLock
首先是一個顯式鎖,它實現了Lock
介面。可能你已經忘記了Lock
介面長啥樣了,我們再回顧一遍:
public interface Lock {
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void unlock();
Condition newCondition();
}
複製程式碼
其實ReentrantLock
內部定義了一個AQS
的子類來輔助它實現鎖的功能,由於ReentrantLock
是工作在獨佔模式
下的,所以它的lock
方法其實是呼叫AQS
物件的aquire
方法去獲取同步狀態,unlock
方法其實是呼叫AQS
物件的release
方法去釋放同步狀態,這些大家已經很熟了,就不再贅述了,我們大致看一下ReentrantLock
的程式碼:
public class ReentrantLock implements Lock {
private final Sync sync; //AQS子類物件
abstract static class Sync extends AbstractQueuedSynchronizer {
// ... 為節省篇幅,省略其他內容
}
// ... 為節省篇幅,省略其他內容
}
複製程式碼
所以如果我們簡簡單單寫下下邊這行程式碼:
Lock lock = new ReentrantLock();
複製程式碼
就意味著在記憶體裡建立了一個ReentrantLock
物件,一個AQS
物件,在AQS
物件裡維護著同步佇列
的head
節點和tail
節點,不過初始狀態下由於沒有執行緒去競爭鎖,所以同步佇列
是空的,畫成圖就是這樣:
Condition的提出
我們前邊嘮叨執行緒間通訊的時候提到過內建鎖的wait/notify
機制,等待執行緒
的典型的程式碼如下:
synchronized (物件) {
處理邏輯(可選)
while(條件不滿足) {
物件.wait();
}
處理邏輯(可選)
}
複製程式碼
通知執行緒的典型的程式碼如下:
synchronized (物件) {
完成條件
物件.notifyAll();、
}
複製程式碼
也就是當一個執行緒因為某個條件不能滿足時就可以在持有鎖的情況下呼叫該鎖物件的wait
方法,之後該執行緒會釋放鎖並進入到與該鎖物件關聯的等待佇列中等待;如果某個執行緒完成了該等待條件,那麼在持有相同鎖的情況下呼叫該鎖的notify
或者notifyAll
方法喚醒在與該鎖物件關聯的等待佇列中等待的執行緒。
顯式鎖的本質其實是通過AQS
物件獲取和釋放同步狀態,而內建鎖的實現是被封裝在java虛擬機器裡的,我們並沒有講過,這兩者的實現是不一樣的。而wait/notify
機制只適用於內建鎖,在顯式鎖
裡需要另外定義一套類似的機制,在我們定義這個機制的時候需要整清楚:在獲取鎖的執行緒因為某個條件不滿足時,應該進入哪個等待佇列,在什麼時候釋放鎖,如果某個執行緒完成了該等待條件,那麼在持有相同鎖的情況下怎麼從相應的等待佇列中將等待的執行緒從佇列中移出。
為了定義這個等待佇列,設計java的大叔們在AQS
中新增了一個名叫ConditionObject
的成員內部類:
public abstract class AbstractQueuedSynchronizer {
public class ConditionObject implements Condition, java.io.Serializable {
private transient Node firstWaiter;
private transient Node lastWaiter;
// ... 為省略篇幅,省略其他方法
}
}
複製程式碼
很顯然,這個ConditionObject
維護了一個佇列,firstWaiter
是佇列的頭節點引用,lastWaiter
是佇列的尾節點引用。但是節點類是Node
?對,你沒看錯,就是我們前邊分析的同步佇列
裡用到的AQS
的靜態內部類Node
,怕你忘了,再把這個Node
節點類的主要內容寫一遍:
static final class Node {
volatile int waitStatus;
volatile Node prev;
volatile Node next;
volatile Thread thread;
Node nextWaiter;
static final int CANCELLED = 1;
static final int SIGNAL = -1;
static final int CONDITION = -2;
static final int PROPAGATE = -3;
}
複製程式碼
也就是說:AQS
中的同步佇列和自定義的等待佇列使用的節點類是同一個。
又由於在等待佇列中的執行緒被喚醒的時候需要重新獲取鎖,也就是重新獲取同步狀態,所以該等待佇列必須知道執行緒是在持有哪個鎖的時候開始等待的。設計java的大叔們在Lock
介面中提供了這麼一個通過鎖來獲取等待佇列的方法:
Condition newCondition();
複製程式碼
我們上邊介紹的ConditionObject
就實現了Condition
介面,看一下ReentrantLock
鎖是怎麼獲取與它相關的等待佇列的:
public class ReentrantLock implements Lock {
private final Sync sync;
abstract static class Sync extends AbstractQueuedSynchronizer {
final ConditionObject newCondition() {
return new ConditionObject();
}
// ... 為節省篇幅,省略其他方法
}
public Condition newCondition() {
return sync.newCondition();
}
// ... 為節省篇幅,省略其他方法
}
複製程式碼
可以看到,其實就是簡單建立了一個ConditionObject
物件而已~ 由於 ConditionObject 是AQS 的成員內部類,所以在建立的 ConditionObject 物件中持有 AQS 物件的引用,所以通過 ConditionObject 物件訪問到 同步佇列,也就是可以重新獲取同步狀態,也就是重新獲取鎖 。用文字描述還是有些繞,我們先通過鎖來建立一個Condition
物件:
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
複製程式碼
由於在初始狀態下,沒有執行緒去競爭鎖,所以同步佇列
是空的,也沒有執行緒因某個條件不成立而進入等待佇列,所以等待佇列
也是空的,ReentrantLock
物件、AQS
物件以及等待佇列在記憶體中的表示就如圖:
當然,這個newCondition
方法可以反覆呼叫,從而可以通過一個鎖來生成多個等待佇列
:
Lock lock = new ReentrantLock();
Condition condition1 = lock.newCondition();
Condition condition2 = lock.newCondition();
複製程式碼
那接下來需要考慮怎麼把執行緒包裝成Node
節點放到等待佇列的以及怎麼從等待佇列中移出了。ConditionObject
成員內部類實現了一個Condition
的介面,這個介面提供了下邊這些方法:
public interface Condition {
void await() throws InterruptedException;
long awaitNanos(long nanosTimeout) throws InterruptedException;
boolean await(long time, TimeUnit unit) throws InterruptedException;
boolean awaitUntil(Date deadline) throws InterruptedException;
void awaitUninterruptibly();
void signal();
void signalAll();
}
複製程式碼
來看一下這些方法的具體意思:
方法名 | 描述 |
---|---|
void await() |
當前執行緒進入等待狀態,直到被通知(呼叫signal或者signalAll方法)或中斷 |
boolean await(long time, TimeUnit unit) |
當前執行緒在指定時間內進入等待狀態,如果超出指定時間或者在等待狀態中被通知或中斷則返回 |
long awaitNanos(long nanosTimeout) |
與上個方法相同,只不過預設使用的時間單位為納秒 |
boolean awaitUntil(Date deadline) |
當前執行緒進入等待狀態,如果到達最後期限或者在等待狀態中被通知或中斷則返回 |
void awaitUninterruptibly() |
當前執行緒進入等待狀態,直到在等待狀態中被通知,需要注意的時,本方法並不相應中斷 |
void signal() |
喚醒一個等待執行緒。 |
void signalAll() |
喚醒所有等待執行緒。 |
可以看到,Condition
中的await
方法和內建鎖物件的wait
方法的作用是一樣的,都會使當前執行緒進入等待狀態,signal
方法和內建鎖物件的notify
方法的作用是一樣的,都會喚醒在等待佇列中的執行緒。
像呼叫內建鎖的wait/notify
方法時,執行緒需要首先獲取該鎖一樣,呼叫Condition
物件的await/siganl
方法的執行緒需要首先獲得產生該Condition
物件的顯式鎖。它的基本使用方式就是:通過顯式鎖的 newCondition 方法產生Condition
物件,執行緒在持有該顯式鎖的情況下可以呼叫生成的Condition
物件的 await/signal 方法,一般用法如下:
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
//等待執行緒的典型模式
public void conditionAWait() throws InterruptedException {
lock.lock(); //獲取鎖
try {
while (條件不滿足) {
condition.await(); //使執行緒處於等待狀態
}
條件滿足後執行的程式碼;
} finally {
lock.unlock(); //釋放鎖
}
}
//通知執行緒的典型模式
public void conditionSignal() throws InterruptedException {
lock.lock(); //獲取鎖
try {
完成條件;
condition.signalAll(); //喚醒處於等待狀態的執行緒
} finally {
lock.unlock(); //釋放鎖
}
}
複製程式碼
假設現在有一個鎖和兩個等待佇列:
Lock lock = new ReentrantLock();
Condition condition1 = lock.newCondition();
Condition condition2 = lock.newCondition();
複製程式碼
畫圖表示出來就是:
有3個執行緒main
、t1
、t2
同時呼叫ReentrantLock
物件的lock
方法去競爭鎖的話,只有執行緒main
獲取到了鎖,所以會把執行緒t1
、t2
包裝成Node
節點插入同步佇列
,所以ReentrantLock
物件、AQS
物件和同步佇列
的示意圖就是這樣的:
因為此時main
執行緒是獲取到鎖處於執行中狀態,但是因為某個條件不滿足,所以它選擇執行下邊的程式碼來進入condition1
等待佇列:
lock.lock();
try {
contition1.await();
} finally {
lock.unlock();
}
複製程式碼
具體的await
程式碼我們就不分析了,太長了,我怕你看的發睏,這裡只看這個await
方法做了什麼事情:
-
在
condition1
等待佇列中建立一個Node
節點,這個節點的thread
值就是main
執行緒,而且waitStatus
為-2
,也就是靜態變數Node.CONDITION
,表示表示節點在等待佇列中,由於這個節點是代表執行緒main
的,所以就把它叫做main節點
把,新建立的節點長這樣: -
將該節點插入
condition1
等待佇列中: -
因為
main
執行緒還持有者鎖,所以需要釋放鎖之後通知後邊等待獲取鎖的執行緒t
,所以同步佇列
裡的0號節點被刪除,執行緒t
獲取鎖,節點1
稱為head
節點,並且把thread
欄位設定為null:
至此,main
執行緒的等待操作就做完了,假如現在獲得鎖的t1
執行緒也執行下邊的程式碼:
lock.lock();
try {
contition1.await();
} finally {
lock.unlock();
}
複製程式碼
還是會執行上邊的過程,把t1
執行緒包裝成Node
節點插入到condition1
等待佇列中去,由於原來在等待佇列中的節點1
會被刪除,我們把這個新插入等待佇列代表執行緒t1
的節點稱為新節點1
吧:
這裡需要特別注意的是:同步佇列是一個雙向連結串列,prev表示前一個節點,next表示後一個節點,而等待佇列是一個單向連結串列,使用nextWaiter表示下一個節點,這是它們不同的地方。
現在獲取到鎖的執行緒是t2
,大家一起出來混的,前兩個都進去,只剩下t2
多不好呀,不過這次不放在condition1
佇列後頭了,換成condition2
佇列吧:
lock.lock();
try {
contition2.await();
} finally {
lock.unlock();
}
複製程式碼
效果就是:
大家發現,雖然現在沒有執行緒獲取鎖,也沒有執行緒在鎖上等待,但是同步佇列
裡仍舊有一個節點,是的,同步佇列只有初始時無任何執行緒因為鎖而阻塞的時候才為空,只要曾經有執行緒因為獲取不到鎖而阻塞,這個佇列就不為空了。
至此,main
、t1
和t2
這三個執行緒都進入到等待狀態了,都進去了誰把它們弄出來呢???額~ 好吧,再弄一個別的執行緒去獲取同一個鎖,比方說執行緒t3
去把condition2
條件佇列的執行緒去喚醒,可以呼叫這個signal
方法:
lock.lock();
try {
contition2.signal();
} finally {
lock.unlock();
}
複製程式碼
因為在condition2
等待佇列的執行緒只有t2
,所以t2
會被喚醒,這個過程分兩步進行:
-
將在
condition2
等待佇列的代表執行緒t2
的新節點2
,從等待佇列中移出。 -
將移出的
節點2
放在同步佇列中等待獲取鎖,同時更改該節點的waitStauts
為0
。
這個過程的圖示如下:
如果執行緒t3
繼續呼叫signalAll
把condition1
等待佇列中的執行緒給喚醒也是差不多的意思,只不過會把condition1
上的兩個節點同時都移動到同步佇列裡:
lock.lock();
try {
contition1.signalAll();
} finally {
lock.unlock();
}
複製程式碼
效果如圖:
這樣全部執行緒都從等待
狀態中恢復了過來,可以重新競爭鎖進行下一步操作了。
以上就是Condition
機制的原理和用法,它其實是內建鎖的wait/notify
機制在顯式鎖中的另一種實現,不過原來的一個內建鎖物件只能對應一個等待佇列,現在一個顯式鎖可以產生若干個等待佇列,我們可以根據執行緒的不同等待條件來把執行緒放到不同的等待佇列上去。Condition
機制的用途可以參考wait/notify
機制,我們接下來把之前用內建鎖和wait/notify
機制編寫的同步佇列BlockedQueue
用顯式鎖 + Condition
的方式來該寫一下:
import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ConditionBlockedQueue<E> {
private Lock lock = new ReentrantLock();
private Condition notEmptyCondition = lock.newCondition();
private Condition notFullCondition = lock.newCondition();
private Queue<E> queue = new LinkedList<>();
private int limit;
public ConditionBlockedQueue(int limit) {
this.limit = limit;
}
public int size() {
lock.lock();
try {
return queue.size();
} finally {
lock.unlock();
}
}
public boolean add(E e) throws InterruptedException {
lock.lock();
try {
while (size() >= limit) {
notFullCondition.await();
}
boolean result = queue.add(e);
notEmptyCondition.signal();
return result;
} finally {
lock.unlock();
}
}
public E remove() throws InterruptedException{
lock.lock();
try {
while (size() == 0) {
notEmptyCondition.await();
}
E e = queue.remove();
notFullCondition.signalAll();
return e;
} finally {
lock.unlock();
}
}
}
複製程式碼
在這個佇列裡邊我們用了一個ReentrantLock
鎖,通過這個鎖生成了兩個Condition
物件,notFullCondition
表示佇列未滿的條件,notEmptyCondition
表示佇列未空的條件。當佇列已滿的時候,執行緒會在notFullCondition
上等待,每插入一個元素,會通知在notEmptyCondition
條件上等待的執行緒;當佇列已空的時候,執行緒會在notEmptyCondition
上等待,每移除一個元素,會通知在notFullCondition
條件上等待的執行緒。這樣語義就變得很明顯了。如果你有更多的等待條件,你可以通過顯式鎖生成更多的Condition
物件。而每個內建鎖物件都只能有一個相關聯的等待佇列,這也是顯式鎖對內建鎖的優勢之一。
我們總結一下上邊的用法:每個顯式鎖物件又可以產生若干個Condition
物件,每個Condition
物件都會對應一個等待佇列,所以就起到了一個顯式鎖對應多個等待佇列的效果。
AQS
中其他針對等待佇列的重要方法
除了Condition
物件的await
和signal
方法,AQS
還提供了許多直接訪問這個佇列的方法,它們由都是public final
修飾的:
public abstract class AbstractQueuedSynchronizer {
public final boolean owns(ConditionObject condition)
public final boolean hasWaiters(ConditionObject condition) {}
public final int getWaitQueueLength(ConditionObject condition) {}
public final Collection<Thread> getWaitingThreads(ConditionObject condition) {}
}
複製程式碼
方法名 | 描述 |
---|---|
owns |
查詢是否通過本AQS 物件生成的指定的 ConditionObject物件 |
hasWaiters |
指定的等待佇列裡是否有等待執行緒 |
getWaitQueueLength |
返回正在等待此條件的執行緒數估計值。因為在構造該結果時,多執行緒環境下實際執行緒集合可能發生大的變化 |
getWaitingThreads |
返回正在等待此條件的執行緒集合的估計值。因為在構造該結果時,多執行緒環境下實際執行緒集合可能發生大的變化 |
如果有需要的話,可以在我們自定義的同步工具中使用它們。
題外話
寫文章挺累的,有時候你覺得閱讀挺流暢的,那其實是背後無數次修改的結果。如果你覺得不錯請幫忙轉發一下,萬分感謝~ 這裡是我的公眾號「我們都是小青蛙」,裡邊有更多技術乾貨,時不時扯一下犢子,歡迎關注:
小冊
另外,作者還寫了一本MySQL小冊:《MySQL是怎樣執行的:從根兒上理解MySQL》的連結 。小冊的內容主要是從小白的角度出發,用比較通俗的語言講解關於MySQL進階的一些核心概念,比如記錄、索引、頁面、表空間、查詢優化、事務和鎖等,總共的字數大約是三四十萬字,配有上百幅原創插圖。主要是想降低普通程式設計師學習MySQL進階的難度,讓學習曲線更平滑一點~ 有在MySQL進階方面有疑惑的同學可以看一下: