ReentrantLock與synchronized
ReentrantLock和synchronized同樣都是用於多執行緒同步,它們在功能上有相近之處,但通常而言,ReentrantLock可以用於替代synchronized。
1, ReentrantLock具備synchronized功能
1 static Object monitor = new Object();
2 synchronized(monitor) {
3 //執行程式碼
4 }
5
6 //建立一個重入鎖,並且產生一個條件監視器物件
7 static ReentrantLock lock = new ReentrantLock();
8 static Condition monitor = lock.newCondition();
9 lock.lock();
10 //執行程式碼
11 lock.unlock();
可以注意到,ReentrantLock有顯示鎖物件,鎖物件可以由使用者決定請求鎖和釋放鎖的時機,它們甚至可以不在同一個程式碼塊中,而synchronized並沒有這麼靈活。
synchronized使用的是Object物件內建的監視器,通過Object.wait/Object.notify()等方法對當前執行緒做等待和喚醒操作。synchronized只能有一個監視器,如果呼叫監視器的notifyAll,那麼會喚醒所有執行緒,較為不靈活。
ReentrantLock使用的是條件監視器Condition,通過ReentrantLock.newCondition()方法來獲取。同一個ReentrantLock可以建立多個condition例項。每個Condition維護有自己的等待執行緒waiter佇列,呼叫signalAll只會喚醒自己佇列內的執行緒。與Object.wait()/Object.notify()的使用方式一樣,Condition呼叫await()/signal()系列方法來達到同樣的目的。
監視器的使用需要注意兩點:
1) 監視器的wait和notify操作會改變執行緒在等待佇列裡的狀態,這個狀態是所有執行緒可見的,必須保證執行緒安全,所以一定要有鎖支撐。也就是說,呼叫wait/notify型別的方法時,必須在該監視器觀察的鎖內部執行。
2) 監視器的notify方法並不會直接喚醒執行緒,它只會改變執行緒在等待佇列裡的狀態,真正的喚醒操作是抽象佇列同步器(AQS)完成的,
2, ReentrantLock更靈活
ReentrantLock的靈活性體現在以下幾個方面
ReentrantLock可以指定公平鎖或非公平鎖,而synchronized限制為公平鎖。ReentrantLock預設為非公平鎖。
ReentrantLock的條件監視器較之synchronized更加方便靈活。ObjectMonitor的等待佇列個數僅有一個,而Condition支援多個佇列。ObjectMonitor釋放鎖進入wait或wait timeout狀態,必須響應中斷,Condition可以不響應中斷。
3, 概括比較synchronized和ReentrantLock優劣
ReentrantLock獲取鎖和釋放鎖的操作更加靈活,且具備獨立的條件監視器,等待和喚醒執行緒的操作也更加方便和多樣化,在多執行緒環境下,ReentrantLock的執行效率比synchronized高。
但是,synchronized的存在還是有意義的,程式不僅僅是執行執行更快的操作和更靈活的就會更優秀,還要考慮到維護成本,synchronized具有完備的語義,一個獲得鎖操作就一定會對應一個釋放鎖操作,否則就會有編譯期異常出現,對於語法友好來講,synchronized可維護性更高。
4, ReentrantLock的條件監視器
Condition,即條件,這個類在AQS裡起到的是監視器monitor的作用,監視器是用於監控一段同步的程式碼塊,可以用於執行緒的阻塞和解除阻塞。
每當條件監視器增加一個等待執行緒的時候,該執行緒也會進入一個條件等待佇列,下次signal方法呼叫的時候,會從佇列裡獲取節點,挨個喚醒。
Condition核心方法:
a) await():當前執行緒進入等待狀態,直到響應通知SIGNAL或者中斷Interrupt。
b) awaitUninterruptily():當前執行緒進入等待狀態,知道響應通知SIGNAL。
c) awaitNanos(long):指定一個納秒為單位的超時時長,當前執行緒進入等待狀態,直到響應通知、中斷或者超時,其返回值為剩餘時間,小於0則超時。
d) awaitUntil(Date):制定一個超時時刻,當前執行緒進入等待狀態,知道響應通知、中斷或者超時
e) signal/signalAll:對condition佇列中的執行緒進行喚醒/喚醒全部
從這些方法可以看出condition方法共分為兩類:
1) await:等效於Object.wait。
2) signal:等效於Object.notify。
wait和notify是Object提供的native方法,Condition為了與Object的方法區分而另行命名的。
以AQS的Condition實現類ConditionObject為例,ConditionObject維護了一個雙向waiter佇列,下面兩個屬性記錄了它的首尾節點。
1 //條件佇列頭結點
2 private transient Node firstWaiter;
3 //條件佇列尾結點
4 private transient Node lastWaiter;
Node節點物件為一個雙向連結串列節點,其資料域為執行緒的引用。
await方法的實現
1 public final void await() throws InterruptedException {
2 //如果當前執行緒是中斷狀態,那麼丟擲中斷異常
3 if(Thread.interrupted()) {
4 throw new InterruptedException();
5 }
6 //把當前執行緒新增到waiter佇列尾
7 Node node = addConditionWaiter();
8 //釋放當前節點擁有的鎖,因為後面還要新增鎖,不釋放會造成死鎖
9 long savedState = fullyRelease(node);
10 int interruptMode = 0;
11 while(!isOnSyncQuequ(node)) {
12 LockSupport.park(this);
13 if((interruptMode = checkInterruptWhileWaiting(node)) != 0) {
14 break;
15 }
16 }
17 if(acquireQueued(node, savedState) && interruptMode != THROW_IE) {
18 interruptMode = REINTERRUPT;
19 }
20 if(node.nextWaiter != null) {
21 //clean up if cancelled
22 unlinkCancelledWaiters();
23 }
24 if(interruptMode != 0) {
25 reportInteruptAfterWait(interruptMode);
26 }
27 }
需要注意的是,阻塞當前執行緒使用的方法為LockSupport.park(),如果需要喚醒,那麼需要有signal()方法來呼叫LockSupport.unpark(Thread);
signal方法的實現
signal方法用於喚醒Condition等待佇列中的下一個等待節點
1 public final void signal() {
2 //只有獨佔模式才能使用signal,否則丟擲異常
3 if(!isHeldExclusively()) {
4 throw new IllegalMoinitorStateException();
5 }
6 Node first = firstWaiter;
7 if(first != null) {
8 doSignal(first);
9 }
10 }
11 private void doSignal(Node first) {
12 //從等待佇列中移除節點,並嘗試喚醒節點
13 do {
14 if(firstWaiter = first.nextWaiter == null) {
15 lastWaiter = null;
16 }
17 first.nextWaiter = null;
18 }
19 while(!transferForSignal(first) && (first = firstWaiter) != null);
20 }
21 final boolean transferForSignal(Node node) {
22 //如果設定waitStatus失敗,那麼說明節點在signal之前被取消了,此時返回false
23 if(!compareAndSetWaitStatus(node, Node.CONDITION, 0)) {
24 return false;
25 }
26 //這個佇列放到sync佇列的尾部
27 Node p = enq(node);
28 //獲取入隊節點的前驅節點狀態
29 int ws = p.waitStatus;
30 //如果前驅節點取消了,那麼可以直接喚醒當前節點的執行緒
31 //如果前驅結點沒有取消,那麼設定當前節點為SIGNAL,而不是喚醒這個執行緒
32 if(ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL)) {
33 LockSupport.unpark(node.thread);
34 }
35 return true;
36 }