再談AbstractQueuedSynchronizer3:基於AbstractQueuedSynchronizer的併發類實現

五月的倉頡發表於2017-07-03

公平模式ReentrantLock實現原理

前面的文章研究了AbstractQueuedSynchronizer的獨佔鎖和共享鎖,有了前兩篇文章的基礎,就可以乘勝追擊,看一下基於AbstractQueuedSynchronizer的併發類是如何實現的。

ReentrantLock顯然是一種獨佔鎖,首先是公平模式的ReentrantLock,Sync是ReentractLock中的基礎類,繼承自AbstractQueuedSynchronizer,看一下程式碼實現:

 1 abstract static class Sync extends AbstractQueuedSynchronizer {
 2     private static final long serialVersionUID = -5179523762034025860L;
 3 
 4     /**
 5      * Performs {@link Lock#lock}. The main reason for subclassing
 6      * is to allow fast path for nonfair version.
 7      */
 8     abstract void lock();
 9 
10     /**
11      * Performs non-fair tryLock.  tryAcquire is
12      * implemented in subclasses, but both need nonfair
13      * try for trylock method.
14      */
15     final boolean nonfairTryAcquire(int acquires) {
16         final Thread current = Thread.currentThread();
17         int c = getState();
18         if (c == 0) {
19             if (compareAndSetState(0, acquires)) {
20                 setExclusiveOwnerThread(current);
21                 return true;
22             }
23         }
24         else if (current == getExclusiveOwnerThread()) {
25             int nextc = c + acquires;
26             if (nextc < 0) // overflow
27                 throw new Error("Maximum lock count exceeded");
28             setState(nextc);
29             return true;
30         }
31         return false;
32     }
33 
34     protected final boolean tryRelease(int releases) {
35         int c = getState() - releases;
36         if (Thread.currentThread() != getExclusiveOwnerThread())
37             throw new IllegalMonitorStateException();
38         boolean free = false;
39         if (c == 0) {
40             free = true;
41             setExclusiveOwnerThread(null);
42         }
43         setState(c);
44         return free;
45     }
46 
47     protected final boolean isHeldExclusively() {
48         // While we must in general read state before owner,
49         // we don't need to do so to check if current thread is owner
50         return getExclusiveOwnerThread() == Thread.currentThread();
51     }
52 
53     final ConditionObject newCondition() {
54         return new ConditionObject();
55     }
56 
57     // Methods relayed from outer class
58 
59     final Thread getOwner() {
60         return getState() == 0 ? null : getExclusiveOwnerThread();
61     }
62 
63     final int getHoldCount() {
64         return isHeldExclusively() ? getState() : 0;
65     }
66 
67     final boolean isLocked() {
68         return getState() != 0;
69     }
70 
71     /**
72      * Reconstitutes this lock instance from a stream.
73      * @param s the stream
74      */
75     private void readObject(java.io.ObjectInputStream s)
76         throws java.io.IOException, ClassNotFoundException {
77         s.defaultReadObject();
78         setState(0); // reset to unlocked state
79     }
80 }

Sync屬於一個公共類,它是抽象的說明Sync會被繼承,簡單整理一下Sync主要做了哪些事(因為Sync不是ReentrantLock公平鎖的關鍵):

  1. 定義了一個lock方法讓子類去實現,我們平時之所以能呼叫ReentrantLock的lock()方法,就是因為Sync定義了它
  2. 實現了非公平鎖tryAcquira的方法
  3. 實現了tryRelease方法,比較簡單,狀態-1,獨佔鎖的執行緒置空
  4. 實現了isHeldExclusively方法
  5. 定義了newCondition方法,讓開發者可以利用Condition實現通知/等待

接著,看一下公平鎖的實現,FairSync類,它繼承自Sync:

 1 static final class FairSync extends Sync {
 2     private static final long serialVersionUID = -3000897897090466540L;
 3 
 4     final void lock() {
 5         acquire(1);
 6     }
 7 
 8     /**
 9      * Fair version of tryAcquire.  Don't grant access unless
10      * recursive call or no waiters or is first.
11      */
12     protected final boolean tryAcquire(int acquires) {
13         final Thread current = Thread.currentThread();
14         int c = getState();
15         if (c == 0) {
16             if (!hasQueuedPredecessors() &&
17                 compareAndSetState(0, acquires)) {
18                 setExclusiveOwnerThread(current);
19                 return true;
20             }
21         }
22         else if (current == getExclusiveOwnerThread()) {
23             int nextc = c + acquires;
24             if (nextc < 0)
25                 throw new Error("Maximum lock count exceeded");
26             setState(nextc);
27             return true;
28         }
29         return false;
30     }
31 }

整理一下要點:

  1. 每次acquire的時候,state+1,如果當前執行緒lock()之後又lock()了,state不斷+1,相應的unlock()的時候state-1,直到將state減到0為之,說明當前執行緒釋放完所有的狀態,其它執行緒可以競爭
  2. state=0的時候,通過hasQueuedPredecessors方法做一次判斷,hasQueuedPredecessors的實現為"h != t && ((s = h.next) == null || s.thread != Thread.currentThread());",其中h是head、t是tail,由於程式碼中對結果取反,因此取反之後的判斷為"h == t || ((s = h.next) != null && s.thread == Thread.currentThread());",總結起來有兩種情況可以通過!hasQueuedPredecessors()這個判斷:
    1. h==t,h==t的情況為要麼當前FIFO佇列中沒有任何資料要麼只構建出了一個head還沒往後面連過任何一個Node,因此head就是tail
    2. (s = h.next) != null && s.thread == Thread.currentThread(),當前執行緒為正在等待的第一個Node中的執行緒  
  3. 如果沒有執行緒比當前執行緒等待更久去執行acquire操作,那麼通過CAS操作將state從0變為1的執行緒tryAcquire成功
  4. 沒有tryAcquire成功的執行緒,按照tryAcquire的先後順序,構建為一個FIFO佇列,即第一個tryAcquire失敗的排在head的後一位,第二個tryAcquire失敗的排在head的後二位
  5. 當tryAcquire成功的執行緒release完畢,第一個tryAcquire失敗的執行緒第一個嘗試tryAcquire,這就是先到先得,典型的公平鎖

 

非公平模式ReentrantLock實現原理

看完了公平模式ReentrantLock,接著我們看一下非公平模式ReentrantLock是如何實現的。NonfairSync類,同樣是繼承自Sync類,實現為:

 1 static final class NonfairSync extends Sync {
 2     private static final long serialVersionUID = 7316153563782823691L;
 3 
 4     /**
 5      * Performs lock.  Try immediate barge, backing up to normal
 6      * acquire on failure.
 7      */
 8     final void lock() {
 9         if (compareAndSetState(0, 1))
10             setExclusiveOwnerThread(Thread.currentThread());
11         else
12             acquire(1);
13     }
14 
15     protected final boolean tryAcquire(int acquires) {
16         return nonfairTryAcquire(acquires);
17     }
18 }

結合nonfairTryAcquire方法一起講解,nonfairTryAcquire方法的實現為:

 1 final boolean nonfairTryAcquire(int acquires) {
 2     final Thread current = Thread.currentThread();
 3     int c = getState();
 4     if (c == 0) {
 5         if (compareAndSetState(0, acquires)) {
 6             setExclusiveOwnerThread(current);
 7             return true;
 8         }
 9     }
10     else if (current == getExclusiveOwnerThread()) {
11         int nextc = c + acquires;
12         if (nextc < 0) // overflow
13             throw new Error("Maximum lock count exceeded");
14         setState(nextc);
15         return true;
16     }
17     return false;
18 }

看到差別就在於非公平鎖lock()的時候會先嚐試通過CAS看看能不能把state從0變為1(即獲取鎖),如果可以的話,直接獲取鎖而不需要排隊。舉個實際例子就很好理解了:

  1. 執行緒1、執行緒2、執行緒3競爭鎖,執行緒1競爭成功獲取鎖,執行緒2、執行緒3依次排隊
  2. 執行緒1執行完畢,釋放鎖,state變為0,喚醒了第一個排隊的執行緒2
  3. 此時執行緒4來嘗試獲取鎖了,由於執行緒2被喚醒了,因此執行緒2與執行緒4競爭鎖
  4. 執行緒4成功將state從0變為1,執行緒2競爭鎖失敗,繼續park

看到整個過程中,後來的執行緒4反而比先來的執行緒2先獲取鎖,相當於是一種非公平的模式,

那為什麼非公平鎖效率會比公平鎖效率高?上面第(3)步如果執行緒2和執行緒4不競爭鎖就是答案。為什麼這麼說,後面的解釋很重要,希望大家可以理解:

執行緒1是先將state設為0,再去喚醒執行緒2,這兩個過程之間是有時間差的。

那麼如果執行緒1將state設定為0的時候,執行緒4就通過CAS演算法獲取到了鎖,且線上程1喚醒執行緒2之前就已經使用完畢鎖,那麼相當於執行緒2獲取鎖的時間並沒有推遲,線上程1將state設定為0到執行緒1喚醒執行緒2的這段時間裡,反而有執行緒4獲取了鎖執行了任務,這就增加了系統的吞吐量,相當於單位時間處理了更多的任務。

從這段解釋我們也應該能看出來了,非公平鎖比較適合加鎖時間比較短的任務。這是因為加鎖時間長,相當於執行緒1將state設為0並去喚醒執行緒2的這段時間,執行緒4無法完成釋放鎖,那麼執行緒2被喚醒由於沒法獲取到鎖,又被阻塞了,這種喚醒-阻塞的操作會引起執行緒的上下文切換,繼而影響系統的效能。

 

Semaphore實現原理

Semaphore即訊號量,用於控制程式碼塊的併發數,將Semaphore的permits設定為1相當於就是synchronized或者ReentrantLock,Semaphore具體用法可見Java多執行緒19:多執行緒下的其他元件之CountDownLatch、Semaphore、Exchanger訊號量允許多條執行緒獲取鎖,顯然它的鎖是一種共享鎖,訊號量也有公平模式與非公平模式,相信看懂了上面ReentrantLock的公平模式與非公平模式的朋友應該對Semaphore的公平模式與非公平模式理解起來會更快,這裡就放在一起寫了。

首先還是看一下Semaphore的基礎設施,它和ReentrantLock一樣,也有一個Sync:

 1 abstract static class Sync extends AbstractQueuedSynchronizer {
 2     private static final long serialVersionUID = 1192457210091910933L;
 3 
 4     Sync(int permits) {
 5         setState(permits);
 6     }
 7 
 8     final int getPermits() {
 9         return getState();
10     }
11 
12     final int nonfairTryAcquireShared(int acquires) {
13         for (;;) {
14             int available = getState();
15             int remaining = available - acquires;
16             if (remaining < 0 ||
17                 compareAndSetState(available, remaining))
18                 return remaining;
19         }
20     }
21 
22     protected final boolean tryReleaseShared(int releases) {
23         for (;;) {
24             int current = getState();
25             int next = current + releases;
26             if (next < current) // overflow
27                 throw new Error("Maximum permit count exceeded");
28             if (compareAndSetState(current, next))
29                 return true;
30         }
31     }
32 
33     final void reducePermits(int reductions) {
34         for (;;) {
35             int current = getState();
36             int next = current - reductions;
37             if (next > current) // underflow
38                 throw new Error("Permit count underflow");
39             if (compareAndSetState(current, next))
40                 return;
41         }
42     }
43 
44     final int drainPermits() {
45         for (;;) {
46             int current = getState();
47             if (current == 0 || compareAndSetState(current, 0))
48                 return current;
49         }
50     }
51 }

和ReentrantLock的Sync差不多,Semaphore的Sync定義了以下的一些主要內容:

  1. getPermits方法獲取當前的許可剩餘量還剩多少,即還有多少執行緒可以同時獲得訊號量
  2. 定義了非公平訊號量獲取共享鎖的邏輯nonfairTryAcquireShared
  3. 定義了公平模式釋放訊號量的邏輯tryReleaseShared,相當於釋放一次訊號量,state就向上+1(訊號量每次的獲取與釋放都是以1為單位的)

再看下公平訊號量的實現,同樣的FairSync,繼承自Sync,程式碼為:

 1 static final class FairSync extends Sync {
 2     private static final long serialVersionUID = 2014338818796000944L;
 3 
 4     FairSync(int permits) {
 5         super(permits);
 6     }
 7 
 8     protected int tryAcquireShared(int acquires) {
 9         for (;;) {
10             if (hasQueuedPredecessors())
11                 return -1;
12             int available = getState();
13             int remaining = available - acquires;
14             if (remaining < 0 ||
15                 compareAndSetState(available, remaining))
16                 return remaining;
17         }
18     }
19 }

首先第10行的hasQueuedPredecessors方法,前面已經說過了,如果已經有了FIFO佇列或者當前執行緒不是FIFO佇列中在等待的第一條執行緒,返回-1,表示無法獲取共享鎖成功。

接著獲取available,available就是state,用volatile修飾,所以執行緒中可以看到最新的state,訊號量的acquires是1,每次獲取訊號量都對state-1,兩種情況直接返回:

  1. remaining減完<0
  2. 通過cas設定成功

之後就是和之前說過的共享鎖的邏輯了,如果返回的是一個<0的數字,那麼構建FIFO佇列,執行緒阻塞,直到前面的執行完才能喚醒後面的。

接著看一下非公平訊號量的實現,NonfairSync繼承Sync:

 1 static final class NonfairSync extends Sync {
 2     private static final long serialVersionUID = -2694183684443567898L;
 3 
 4     NonfairSync(int permits) {
 5         super(permits);
 6     }
 7 
 8     protected int tryAcquireShared(int acquires) {
 9         return nonfairTryAcquireShared(acquires);
10     }
11 }

nonfairTryAcquireShared在父類已經實現了,再貼一下程式碼:

1 final int nonfairTryAcquireShared(int acquires) {
2     for (;;) {
3         int available = getState();
4         int remaining = available - acquires;
5         if (remaining < 0 ||
6             compareAndSetState(available, remaining))
7             return remaining;
8     }
9 }

看到這裡和公平Semaphore只有一點差別:不會前置進行一次hasQueuedPredecessors()判斷。即當前有沒有構建為一個FIFO佇列,佇列裡面第一個等待的執行緒是不是自身都無所謂,對於非公平Semaphore都一樣,反正執行緒呼叫Semaphore的acquire方法就將當前state-1,如果得到的remaining設定成功或者CAS操作成功就返回,這種操作沒有遵循先到先得的原則,即非公平訊號量。

至於非公平訊號量對比公平訊號量的優點,和ReentrantLock的非公平鎖對比ReentrantLock的公平鎖一樣,就不說了。

 

CountDownLatch實現原理

CountDownLatch即計數器自減的一種閉鎖,某執行緒阻塞,對一個計數器自減到0,此執行緒被喚醒,CountDownLatch具體用法可見Java多執行緒19:多執行緒下的其他元件之CountDownLatch、Semaphore、Exchanger

CountDownLatch是一種共享鎖,通過await()方法與countDown()兩個方法實現自身的功能,首先看一下await()方法的實現:

 1 public void await() throws InterruptedException {
 2     sync.acquireSharedInterruptibly(1);
 3 }

acquireSharedInterruptibly最終又回到tryAcquireShared方法上,直接貼整個Sync的程式碼實現:

 1 private static final class Sync extends AbstractQueuedSynchronizer {
 2     private static final long serialVersionUID = 4982264981922014374L;
 3 
 4     Sync(int count) {
 5         setState(count);
 6     }
 7 
 8     int getCount() {
 9         return getState();
10     }
11 
12     protected int tryAcquireShared(int acquires) {
13         return (getState() == 0) ? 1 : -1;
14     }
15 
16     protected boolean tryReleaseShared(int releases) {
17         // Decrement count; signal when transition to zero
18         for (;;) {
19             int c = getState();
20             if (c == 0)
21                 return false;
22             int nextc = c-1;
23             if (compareAndSetState(c, nextc))
24                 return nextc == 0;
25         }
26     }
27 }

其實看到tryAcquireShared方法,理解AbstractQueuedSynchronizer共享鎖原理的,不用看countDown方法應該都能猜countDown方法是如何實現的。我這裡總結一下:

  1. 傳入一個count,state就等於count,await的時候判斷是不是0,是0返回1表示成功,不是0返回-1表示失敗,構建FIFO佇列,head頭只連線一個Node,Node中的執行緒就是呼叫CountDownLatch的await()方法的執行緒
  2. 每次countDown的時候對state-1,直到state減到0的時候才算tryReleaseShared成功,tryReleaseShared成功,喚醒被掛起的執行緒

為了驗證(2),看一下上面Sync的tryReleaseShared方法就可以了,確實是這麼實現的。

 

再理解獨佔鎖與共享鎖的區別

本文詳細分析了ReentrantLock、Semaphore、CountDownLatch的實現原理,第一個是基於獨佔鎖的實現,後兩個是基於共享鎖的實現,從這三個類我們可以再總結一下獨佔鎖與共享鎖的區別,主要在兩點上:

  1. 獨佔鎖同時只有一條執行緒可以acquire成功,共享鎖同時可能有多條執行緒可以acquire成功,Semaphore是典型例子
  2. 獨佔鎖每次只能喚醒一個Node,共享鎖每次喚醒的時候可以將狀態向後傳播,即可能喚醒多個Node,CountDownLatch是典型例子

帶著這兩個結論再看ReentrantLock、Semaphore、CountDownLatch,你一定會對獨佔鎖與共享鎖理解更深。

相關文章