同步狀態
AQS採用的是CLH隊列,CLH隊列是由一個一個結點構成的,前面提到結點中有一個狀態位,這個狀態位與線程狀態密切相關,這個狀態位(waitStatus)是一個32位的整型常量,它的取值如下:
- static final int CANCELLED = 1;
- static final int SIGNAL = -1;
- static final int CONDITION = -2;
- static final int PROPAGATE = -3;
CANCELLED:因爲超時或者中斷,結點會被設置爲取消狀態,被取消狀態的結點不應該去競爭鎖,只能保持取消狀態不變,不能轉換爲其他狀態。處於這種狀態的結點會被踢出隊列,被GC回收;
SIGNAL:表示這個結點的繼任結點被阻塞了,到時需要通知它;
CONDITION:表示這個結點在條件隊列中,因爲等待某個條件而被阻塞;
PROPAGATE:使用在共享模式頭結點有可能牌處於這種狀態,表示鎖的下一次獲取可以無條件傳播;
0:None of the above,新結點會處於這種狀態。
獲取
AQS中比較重要的兩個操作是獲取和釋放,以下是各種獲取操作:
- public final void acquire(int arg);
- public final void acquireInterruptibly(int arg);
- public final void acquireShared(int arg);
- public final void acquireSharedInterruptibly(int arg);
- protected boolean tryAcquire(int arg);
- protected int tryAcquireShared(int arg);
- public final boolean tryAcquireNanos(int arg, long nanosTimeout) throws InterruptedException;
- public final boolean tryAcquireSharedNanos(int arg, long nanosTimeout) throws InterruptedException;
1、如果嘗試獲取鎖成功整個獲取操作就結束,否則轉到2. 嘗試獲取鎖是通過方法tryAcquire來實現的,AQS中並沒有該方法的具體實現,只是簡單地拋出一個不支持操作異常,在AQS簡介中談到tryAcquire有很多實現方法,這裏不再細化,只需要知道如果獲取鎖成功該方法返回true即可;
2、如果獲取鎖失敗,那麼就創建一個代表當前線程的結點加入到等待隊列的尾部,是通過addWaiter方法實現的,來看該方法的具體實現:
- /**
- * Creates and enqueues node for current thread and given mode.
- *
- * @param mode Node.EXCLUSIVE for exclusive, Node.SHARED for shared
- * @return the new node
- */
- private Node addWaiter(Node mode) {
- Node node = new Node(Thread.currentThread(), mode);
- // Try the fast path of enq; backup to full enq on failure
- Node pred = tail;
- if (pred != null) {
- node.prev = pred;
- if (compareAndSetTail(pred, node)) {
- pred.next = node;
- return node;
- }
- }
- enq(node);
- return node;
- }
如果沒有元素(pred==null),表示隊列爲空,走的是入隊操作
- /**
- * Inserts node into queue, initializing if necessary. See picture above.
- * @param node the node to insert
- * @return node's predecessor
- */
- private Node enq(final Node node) {
- for (;;) {
- Node t = tail;
- if (t == null) { // Must initialize
- if (compareAndSetHead(new Node()))
- tail = head;
- } else {
- node.prev = t;
- if (compareAndSetTail(t, node)) {
- t.next = node;
- return t;
- }
- }
- }
- }
如果頭結點非空,就採用CAS操作將當前結點插入到頭結點後面,如果在插入的時候尾結點有變化,就將尾結點向後移動直到移動到最後一個結點爲止,然後再把當前結點插入到尾結點後面,尾指針指向當前結點,入隊成功。
3、將新加入的結點放入隊列之後,這個結點有兩種狀態,要麼獲取鎖,要麼就掛起,如果這個結點不是頭結點,就看看這個結點是否應該掛起,如果應該掛起,就掛起當前結點,是否應該掛起是通過shouldParkAfterFailedAcquire方法來判斷的
- /**
- * Checks and updates status for a node that failed to acquire.
- * Returns true if thread should block. This is the main signal
- * control in all acquire loops. Requires that pred == node.prev
- *
- * @param pred node's predecessor holding status
- * @param node the node
- * @return {@code true} if thread should block
- */
- private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
- int ws = pred.waitStatus;
- if (ws == Node.SIGNAL)
- /*
- * This node has already set status asking a release
- * to signal it, so it can safely park.
- */
- return true;
- if (ws > 0) {
- /*
- * Predecessor was cancelled. Skip over predecessors and
- * indicate retry.
- */
- do {
- node.prev = pred = pred.prev;
- } while (pred.waitStatus > 0);
- pred.next = node;
- } else {
- /*
- * waitStatus must be 0 or PROPAGATE. Indicate that we
- * need a signal, but don't park yet. Caller will need to
- * retry to make sure it cannot acquire before parking.
- */
- compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
- }
- return false;
- }
如果前趨結點是一個被取消的結點怎麼辦呢?那麼就向前遍歷跳過被取消的結點,直到找到一個沒有被取消的結點爲止,將找到的這個結點作爲它的前趨結點,將找到的這個結點的waitStatus位設置爲SIGNAL,返回false表示線程不應該被掛起。
上面談的不是頭結點的情況決定是否應該掛起,是頭結點的情況呢?
是頭結點的情況,當前線程就調用tryAcquire嘗試獲取鎖,如果獲取成功就將頭結點設置爲當前結點,返回;如果獲取失敗就循環嘗試獲取鎖,直到獲取成功爲止。整個acquire過程就分析完了。
釋放
釋放操作有以下方法:
- public final boolean release(int arg);
- protected boolean tryRelease(int arg);
- protected boolean tryReleaseShared(int arg);
下面看看release方法的實現過程
1、release過程比acquire要簡單,首先調用tryRelease釋放鎖,如果釋放失敗,直接返回;
2、釋放鎖成功後需要喚醒繼任結點,是通過方法unparkSuccessor實現的
- /**
- * Wakes up node's successor, if one exists.
- *
- * @param node the node
- */
- private void unparkSuccessor(Node node) {
- /*
- * If status is negative (i.e., possibly needing signal) try
- * to clear in anticipation of signalling. It is OK if this
- * fails or if status is changed by waiting thread.
- */
- int ws = node.waitStatus;
- if (ws < 0)
- compareAndSetWaitStatus(node, ws, 0);
- /*
- * Thread to unpark is held in successor, which is normally
- * just the next node. But if cancelled or apparently null,
- * traverse backwards from tail to find the actual
- * non-cancelled successor.
- */
- Node s = node.next;
- if (s == null || s.waitStatus > 0) {
- s = null;
- for (Node t = tail; t != null && t != node; t = t.prev)
- if (t.waitStatus <= 0)
- s = t;
- }
- if (s != null)
- LockSupport.unpark(s.thread);
- }
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方法判斷一個結點是否在同步隊列,實現如下:
- /**
- * Returns true if a node, always one that was initially placed on
- * a condition queue, is now waiting to reacquire on sync queue.
- * @param node the node
- * @return true if is reacquiring
- */
- final boolean isOnSyncQueue(Node node) {
- if (node.waitStatus == Node.CONDITION || node.prev == null)
- return false;
- if (node.next != null) // If has successor, it must be on queue
- return true;
- /*
- * node.prev can be non-null, but not yet on queue because
- * the CAS to place it on queue can fail. So we have to
- * traverse from tail to make sure it actually made it. It
- * will always be near the tail in calls to this method, and
- * unless the CAS failed (which is unlikely), it will be
- * there, so we hardly ever traverse much.
- */
- return findNodeFromTail(node);
- }
因此將CANCEL結點的後繼指向它自己是合理的選擇。