AQS源碼分析(獲取與釋放)

同步狀態

AQS採用的是CLH隊列,CLH隊列是由一個一個結點構成的,前面提到結點中有一個狀態位,這個狀態位與線程狀態密切相關,這個狀態位(waitStatus)是一個32位的整型常量,它的取值如下:

[java] view plain copy
 print?
  1. static final int CANCELLED =  1;  
  2. static final int SIGNAL    = -1;  
  3. static final int CONDITION = -2;  
  4. static final int PROPAGATE = -3;  
下面解釋一下每個值的含義

CANCELLED:因爲超時或者中斷,結點會被設置爲取消狀態,被取消狀態的結點不應該去競爭鎖,只能保持取消狀態不變,不能轉換爲其他狀態。處於這種狀態的結點會被踢出隊列,被GC回收;

SIGNAL:表示這個結點的繼任結點被阻塞了,到時需要通知它;

CONDITION:表示這個結點在條件隊列中,因爲等待某個條件而被阻塞;

PROPAGATE:使用在共享模式頭結點有可能牌處於這種狀態,表示鎖的下一次獲取可以無條件傳播;

0:None of the above,新結點會處於這種狀態。


獲取

AQS中比較重要的兩個操作是獲取和釋放,以下是各種獲取操作:

[java] view plain copy
 print?
  1. public final void acquire(int arg);  
  2. public final void acquireInterruptibly(int arg);  
  3. public final void acquireShared(int arg);  
  4. public final void acquireSharedInterruptibly(int arg);  
  5. protected boolean tryAcquire(int arg);   
  6. protected int tryAcquireShared(int arg);  
  7. public final boolean tryAcquireNanos(int arg, long nanosTimeout) throws InterruptedException;  
  8. public final boolean tryAcquireSharedNanos(int arg, long nanosTimeout) throws InterruptedException;       
獲取操作的流程圖如下:

1、如果嘗試獲取鎖成功整個獲取操作就結束,否則轉到2. 嘗試獲取鎖是通過方法tryAcquire來實現的,AQS中並沒有該方法的具體實現,只是簡單地拋出一個不支持操作異常,在AQS簡介中談到tryAcquire有很多實現方法,這裏不再細化,只需要知道如果獲取鎖成功該方法返回true即可;

2、如果獲取鎖失敗,那麼就創建一個代表當前線程的結點加入到等待隊列的尾部,是通過addWaiter方法實現的,來看該方法的具體實現:

[java] view plain copy
 print?
  1. /** 
  2.  * Creates and enqueues node for current thread and given mode. 
  3.  * 
  4.  * @param mode Node.EXCLUSIVE for exclusive, Node.SHARED for shared 
  5.  * @return the new node 
  6.  */  
  7. private Node addWaiter(Node mode) {  
  8.     Node node = new Node(Thread.currentThread(), mode);  
  9.     // Try the fast path of enq; backup to full enq on failure  
  10.     Node pred = tail;  
  11.     if (pred != null) {  
  12.         node.prev = pred;  
  13.         if (compareAndSetTail(pred, node)) {  
  14.             pred.next = node;  
  15.             return node;  
  16.         }  
  17.     }  
  18.     enq(node);  
  19.     return node;  
  20. }  
該方法創建了一個獨佔式結點,然後判斷隊列中是否有元素,如果有(pred!=null)就設置當前結點爲隊尾結點,返回;

如果沒有元素(pred==null),表示隊列爲空,走的是入隊操作

[java] view plain copy
 print?
  1. /** 
  2.  * Inserts node into queue, initializing if necessary. See picture above. 
  3.  * @param node the node to insert 
  4.  * @return node's predecessor 
  5.  */  
  6. private Node enq(final Node node) {  
  7.     for (;;) {  
  8.         Node t = tail;  
  9.         if (t == null) { // Must initialize  
  10.             if (compareAndSetHead(new Node()))  
  11.                 tail = head;  
  12.         } else {  
  13.             node.prev = t;  
  14.             if (compareAndSetTail(t, node)) {  
  15.                 t.next = node;  
  16.                 return t;  
  17.             }  
  18.         }  
  19.     }  
  20. }  
enq方法採用的是變種CLH算法,先看頭結點是否爲空,如果爲空就創建一個傀儡結點,頭尾指針都指向這個傀儡結點,這一步只會在隊列初始化時會執行;

如果頭結點非空,就採用CAS操作將當前結點插入到頭結點後面,如果在插入的時候尾結點有變化,就將尾結點向後移動直到移動到最後一個結點爲止,然後再把當前結點插入到尾結點後面,尾指針指向當前結點,入隊成功。

3、將新加入的結點放入隊列之後,這個結點有兩種狀態,要麼獲取鎖,要麼就掛起,如果這個結點不是頭結點,就看看這個結點是否應該掛起,如果應該掛起,就掛起當前結點,是否應該掛起是通過shouldParkAfterFailedAcquire方法來判斷的

[java] view plain copy
 print?
  1. /** 
  2.     * Checks and updates status for a node that failed to acquire. 
  3.     * Returns true if thread should block. This is the main signal 
  4.     * control in all acquire loops.  Requires that pred == node.prev 
  5.     * 
  6.     * @param pred node's predecessor holding status 
  7.     * @param node the node 
  8.     * @return {@code true} if thread should block 
  9.     */  
  10.    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {  
  11.        int ws = pred.waitStatus;  
  12.        if (ws == Node.SIGNAL)  
  13.            /* 
  14.             * This node has already set status asking a release 
  15.             * to signal it, so it can safely park. 
  16.             */  
  17.            return true;  
  18.        if (ws > 0) {  
  19.            /* 
  20.             * Predecessor was cancelled. Skip over predecessors and 
  21.             * indicate retry. 
  22.             */  
  23.            do {  
  24.                node.prev = pred = pred.prev;  
  25.            } while (pred.waitStatus > 0);  
  26.            pred.next = node;  
  27.        } else {  
  28.            /* 
  29.             * waitStatus must be 0 or PROPAGATE.  Indicate that we 
  30.             * need a signal, but don't park yet.  Caller will need to 
  31.             * retry to make sure it cannot acquire before parking. 
  32.             */  
  33.            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);  
  34.        }  
  35.        return false;  
  36.    }  
該方法首先檢查前趨結點的waitStatus位,如果爲SIGNAL,表示前趨結點會通知它,那麼它可以放心大膽地掛起了;

如果前趨結點是一個被取消的結點怎麼辦呢?那麼就向前遍歷跳過被取消的結點,直到找到一個沒有被取消的結點爲止,將找到的這個結點作爲它的前趨結點,將找到的這個結點的waitStatus位設置爲SIGNAL,返回false表示線程不應該被掛起。
上面談的不是頭結點的情況決定是否應該掛起,是頭結點的情況呢?

是頭結點的情況,當前線程就調用tryAcquire嘗試獲取鎖,如果獲取成功就將頭結點設置爲當前結點,返回;如果獲取失敗就循環嘗試獲取鎖,直到獲取成功爲止。整個acquire過程就分析完了。


釋放

釋放操作有以下方法:

[java] view plain copy
 print?
  1. public final boolean release(int arg);   
  2. protected boolean tryRelease(int arg);   
  3. protected boolean tryReleaseShared(int arg);   

下面看看release方法的實現過程


1、release過程比acquire要簡單,首先調用tryRelease釋放鎖,如果釋放失敗,直接返回;

2、釋放鎖成功後需要喚醒繼任結點,是通過方法unparkSuccessor實現的

[java] view plain copy
 print?
  1. /** 
  2.     * Wakes up node's successor, if one exists. 
  3.     * 
  4.     * @param node the node 
  5.     */  
  6.    private void unparkSuccessor(Node node) {  
  7.        /* 
  8.         * If status is negative (i.e., possibly needing signal) try 
  9.         * to clear in anticipation of signalling.  It is OK if this 
  10.         * fails or if status is changed by waiting thread. 
  11.         */  
  12.        int ws = node.waitStatus;  
  13.        if (ws < 0)  
  14.            compareAndSetWaitStatus(node, ws, 0);  
  15.   
  16.        /* 
  17.         * Thread to unpark is held in successor, which is normally 
  18.         * just the next node.  But if cancelled or apparently null, 
  19.         * traverse backwards from tail to find the actual 
  20.         * non-cancelled successor. 
  21.         */  
  22.        Node s = node.next;  
  23.        if (s == null || s.waitStatus > 0) {  
  24.            s = null;  
  25.            for (Node t = tail; t != null && t != node; t = t.prev)  
  26.                if (t.waitStatus <= 0)  
  27.                    s = t;  
  28.        }  
  29.        if (s != null)  
  30.            LockSupport.unpark(s.thread);  
  31.    }  
1、node參數傳進來的是頭結點,首先檢查頭結點的waitStatus位,如果爲負,表示頭結點還需要通知後繼結點,這裏不需要頭結點去通知後繼,因此將該該標誌位清0.

2、然後查看頭結點的下一個結點,如果下一個結點不爲空且它的waitStatus<=0,表示後繼結點沒有被取消,是一個可以喚醒的結點,於是喚醒後繼結點返回;如果後繼結點爲空或者被取消了怎麼辦?尋找下一個可喚醒的結點,然後喚醒它返回。

這裏並沒有從頭向尾尋找,而是相反的方向尋找,爲什麼呢?

因爲在CLH隊列中的結點隨時有可能被中斷,被中斷的結點的waitStatus設置爲CANCEL,而且它會被踢出CLH隊列,如何個踢出法,就是它的前趨結點的next並不會指向它,而是指向下一個非CANCEL的結點,而它自己的next指針指向它自己。一旦這種情況發生,如何從頭向尾方向尋找繼任結點會出現問題,因爲一個CANCEL結點的next爲自己,那麼就找不到正確的繼任接點。

有的人又會問了,CANCEL結點的next指針爲什麼要指向它自己,爲什麼不指向真正的next結點?爲什麼不爲NULL?

第一個問題的答案是這種被CANCEL的結點最終會被GC回收,如果指向next結點,GC無法回收。

對於第二個問題的回答,JDK中有這麼一句話: The next field of cancelled nodes is set to point to the node itself instead of null, to make life easier for isOnSyncQueue.大至意思是爲了使isOnSyncQueue方法更新簡單。isOnSyncQueue方法判斷一個結點是否在同步隊列,實現如下:

[java] view plain copy
 print?
  1. /** 
  2.  * Returns true if a node, always one that was initially placed on 
  3.  * a condition queue, is now waiting to reacquire on sync queue. 
  4.  * @param node the node 
  5.  * @return true if is reacquiring 
  6.  */  
  7. final boolean isOnSyncQueue(Node node) {  
  8.     if (node.waitStatus == Node.CONDITION || node.prev == null)  
  9.         return false;  
  10.     if (node.next != null// If has successor, it must be on queue  
  11.         return true;  
  12.     /* 
  13.      * node.prev can be non-null, but not yet on queue because 
  14.      * the CAS to place it on queue can fail. So we have to 
  15.      * traverse from tail to make sure it actually made it.  It 
  16.      * will always be near the tail in calls to this method, and 
  17.      * unless the CAS failed (which is unlikely), it will be 
  18.      * there, so we hardly ever traverse much. 
  19.      */  
  20.     return findNodeFromTail(node);  
  21. }  
如果一個結點next不爲空,那麼它在同步隊列中,如果CANCEL結點的後繼爲空那麼CANCEL結點不在同步隊列中,這與事實相矛盾。

因此將CANCEL結點的後繼指向它自己是合理的選擇。

發佈了29 篇原創文章 · 獲贊 19 · 訪問量 14萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章