AQS源碼分析(共享與互斥)

共享模式與獨佔模式

AQL的內部隊列採用的是CLH隊列鎖模型,CLH隊列是由一個一個結點(Node)構成的。Node類中有兩個常量SHARE和EXCLUSIVE,顧名思義這兩個常量用於表示這個結點支持共享模式還是獨佔模式,共享模式指的是允許多個線程獲取同一個鎖而且可能獲取成功,獨佔模式指的是一個鎖如果被一個線程持有,其他線程必須等待。多個線程讀取一個文件可以採用共享模式,而當有一個線程在寫文件時不會允許另一個線程寫這個文件,這就是獨佔模式的應用場景。

[java] view plain copy
 print?
  1. /** Marker to indicate a node is waiting in shared mode */  
  2. static final Node SHARED = new Node();  
  3.   
  4. /** Marker to indicate a node is waiting in exclusive mode */  
  5. static final Node EXCLUSIVE = null;  
  6.   
  7. final boolean isShared() {  
  8.     return nextWaiter == SHARED;  
  9. }  
以上代碼是兩種模式的定義,可以通過方法isShared來判斷一個結點處於何種模式。


共享模式下獲取鎖

共享模式下獲取鎖是通過tryAcquireShared方法來實現的,其流程大至如下:

AQS類方法中方法名不含shared的默認是獨佔模式,前面提到子類需要重寫tryAcquire方法,這是在獨佔模式下。如果子類想支持共享模式,同樣必須重寫tryAcquireShared方法,線程首先通過tryAcquireShared方法在共享模式下獲取鎖,如果獲取成功就直接返回,否則執行以下步驟:
[java] view plain copy
 print?
  1. /** 
  2.  * Acquires in shared uninterruptible mode. 
  3.  * @param arg the acquire argument 
  4.  */  
  5. private void doAcquireShared(int arg) {  
  6.     final Node node = addWaiter(Node.SHARED);  
  7.     boolean failed = true;  
  8.     try {  
  9.         boolean interrupted = false;  
  10.         for (;;) {  
  11.             final Node p = node.predecessor();  
  12.             if (p == head) {  
  13.                 int r = tryAcquireShared(arg);  
  14.                 if (r >= 0) {  
  15.                     setHeadAndPropagate(node, r);  
  16.                     p.next = null// help GC  
  17.                     if (interrupted)  
  18.                         selfInterrupt();  
  19.                     failed = false;  
  20.                     return;  
  21.                 }  
  22.             }  
  23.             if (shouldParkAfterFailedAcquire(p, node) &&  
  24.                 parkAndCheckInterrupt())  
  25.                 interrupted = true;  
  26.         }  
  27.     } finally {  
  28.         if (failed)  
  29.             cancelAcquire(node);  
  30.     }  
  31. }  
1、創建一個新結點(共享模式),加入到隊尾,這個過程和獨佔模式一樣,不再重複;
2、判斷新結點的前趨結點是否爲頭結點,如果不是頭結點,就將前趨結點的狀態標誌位設置爲SIGNAL,當前線程可以安全地掛起,整個過程結束;
3、如果它的前趨是頭結點,就讓前趨在共享模式下獲取鎖,如果獲取成功,把當前結點設置爲頭結點;
4、設置爲頭結點之後,滿足釋放鎖條件就阻塞等待釋放鎖。
滿足釋放鎖的條件爲:允許傳播或者需要通知繼任結點,或者繼任結點是共享模式的結點
[java] view plain copy
 print?
  1. if (propagate > 0 || h == null || h.waitStatus < 0) {  
  2.           Node s = node.next;  
  3.           if (s == null || s.isShared())  
  4.               doReleaseShared();  
  5.       }  

共享模式下釋放鎖

這是通過方法releaseShared來實現的,整個流程如下:
1、調用子類的tryReleaseShared嘗試獲取鎖,如果失敗,直接返回;
2、如果成功調用doReleaseShared方法做後續處理,doReleaseShared方法如下:
[java] view plain copy
 print?
  1. /** 
  2.     * Release action for shared mode -- signal successor and ensure 
  3.     * propagation. (Note: For exclusive mode, release just amounts 
  4.     * to calling unparkSuccessor of head if it needs signal.) 
  5.     */  
  6.    private void doReleaseShared() {  
  7.        /* 
  8.         * Ensure that a release propagates, even if there are other 
  9.         * in-progress acquires/releases.  This proceeds in the usual 
  10.         * way of trying to unparkSuccessor of head if it needs 
  11.         * signal. But if it does not, status is set to PROPAGATE to 
  12.         * ensure that upon release, propagation continues. 
  13.         * Additionally, we must loop in case a new node is added 
  14.         * while we are doing this. Also, unlike other uses of 
  15.         * unparkSuccessor, we need to know if CAS to reset status 
  16.         * fails, if so rechecking. 
  17.         */  
  18.        for (;;) {  
  19.            Node h = head;  
  20.            if (h != null && h != tail) {  
  21.                int ws = h.waitStatus;  
  22.                if (ws == Node.SIGNAL) {  
  23.                    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))  
  24.                        continue;            // loop to recheck cases  
  25.                    unparkSuccessor(h);  
  26.                }  
  27.                else if (ws == 0 &&  
  28.                         !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))  
  29.                    continue;                // loop on failed CAS  
  30.            }  
  31.            if (h == head)                   // loop if head changed  
  32.                break;  
  33.        }  
  34.    }  
這個方法就一個目的,就是把當前結點設置爲SIGNAL或者PROPAGATE,如果當前結點不是頭結點也不是尾結點,先判斷當前結點的狀態位是否爲SIGNAL,如果是就設置爲0,因爲共享模式下更多使用PROPAGATE來傳播,SIGNAL會被經過兩步改爲PROPAGATE:
compareAndSetWaitStatus(h, Node.SIGNAL, 0)
compareAndSetWaitStatus(h, 0, Node.PROPAGATE)
爲什麼要經過兩步呢?原因在unparkSuccessor方法:
[java] view plain copy
 print?
  1. private void unparkSuccessor(Node node) {  
  2.     int ws = node.waitStatus;  
  3.     if (ws < 0)  
  4.         compareAndSetWaitStatus(node, ws, 0);  
  5.         ......  
  6. }  
如果直接從SIGNAL到PROPAGATE,那麼到unparkSuccessor方法裏面又被設置爲0:SIGNAL--PROPAGATE---0----PROPAGATE
對頭結點相當於多做了一次compareAndSet操作,其實性能也殊途同歸啦!

閉鎖(CountDownLatch)

閉鎖是一個同步輔助類,在完成一組正在其他線程中執行的操作之前,它允許一個或多個線程一直等待。
閉鎖有幾個重要的方法:
[java] view plain copy
 print?
  1. public void await() throws InterruptedException;  
  2. public void countDown();  
其中await方法使當前線程在鎖存器倒計數至零之前一直等待,除非線程被中斷,如果鎖存器爲0方法立即返回,一開始鎖存器不會爲0,當調用countDown方法之後鎖存器會減少,當鎖存器減少到0時,await方法就會返回。現在看看await方法的實現:
[java] view plain copy
 print?
  1. public void await() throws InterruptedException {  
  2.     sync.acquireSharedInterruptibly(1);  
  3. }  
不出所料,閉鎖的await方法正是使用的共享模式的AQS,acquireSharedInterruptibly和acquireShared方法類似,只不過會先響應中斷。也就是當有多個線程調用await方法時,這些線程都被阻塞到了doAcquireShared方法的以下地方:
[java] view plain copy
 print?
  1. if (shouldParkAfterFailedAcquire(p, node) &&  
  2.                    parkAndCheckInterrupt())  
  3.                    interrupted = true;  
前面看到doAcquireShared裏面有一個for循環,退出for循環的唯一方式是要tryAcquireShared方法返回值大於0,下面看看tryAcquireShared的方法在閉鎖中的實現:
[java] view plain copy
 print?
  1. public class CountDownLatch {  
  2.     private static final class Sync extends AbstractQueuedSynchronizer  {  
  3.         Sync(int count) {  
  4.             setState(count);  
  5.         }  
  6.         ......  
  7.     }  
  8.       
  9.     private final Sync sync;  
  10.           
  11.     protected int tryAcquireShared(int acquires) {  
  12.         return (getState() == 0) ? 1 : -1;  
  13.     }  
  14.     ......  
  15. }  
count代表是的線程數,在創建閉鎖的同步器時這個count值被賦給了state,因此state肯定不爲0,所以tryAcquireShared方法肯定返回-1,也就是這些線程調用await方法時tryAcquireShared都返回-1,這些線程都會阻塞在doAcquireShared的for循環裏。然後這些線程依次調用countDown方法,直到最後一個線程調用完後這些線程纔會退出for循環繼續執行。下面看看countDown方法的實現過程:
[java] view plain copy
 print?
  1. public void countDown() {  
  2.     sync.releaseShared(1);  
  3. }  
  4.   
  5. //sync.releaseShared  
  6. public final boolean releaseShared(int arg) {  
  7.     if (tryReleaseShared(arg)) {  
  8.         doReleaseShared();  
  9.         return true;  
  10.     }  
  11.     return false;  
  12. }  
仍然不出所料,countDown方法正是調用的releaseShared方法,前面提到releaseShared會先調用tryReleaseShared方法,這是由閉鎖實現的:
[java] view plain copy
 print?
  1. protected boolean tryReleaseShared(int releases) {  
  2.     // Decrement count; signal when transition to zero  
  3.     for (;;) {  
  4.         int c = getState();  
  5.         if (c == 0)  
  6.             return false;  
  7.         int nextc = c-1;  
  8.         if (compareAndSetState(c, nextc))  
  9.             return nextc == 0;  
  10.     }  
  11. }  
該方法會遞減state的值,直到變爲0返回false.
現在整個閉鎖的執行流程很明確了:N個線程調用await阻塞在for循環裏面,然後N個線程依次調用countDown,每調用一次state減1,直接state爲0,這些線程退出for循環(解除阻塞)!
退出for循環時,由於頭結點狀態標誌位爲PROPAGATE,而且這些結點都是共享模式,由頭結點一傳播,這些結點都獲取鎖,於是齊頭並進執行了......
共享與獨佔在讀寫鎖裏面也有用到,後面再分析。
發佈了29 篇原創文章 · 獲贊 19 · 訪問量 14萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章