java中鎖的深入理解(二)

上一篇講到了synchronized實現原理,這一篇一起來學習一下Lock的底層實現原理。

java.util.concurrent包中有很多lock接口的實現類,ReentrantLock,ReadWriteLock,這在我的另一篇文章中也提到了。

這些實現類的內部都使用了隊列同步器AbstractQueuedSynchronizer類作爲依賴。下面我們用AQS來指代AbstractQueuedSynchronizer。

AQS定義:

public abstract class AbstractQueuedSynchronizer extends
    AbstractOwnableSynchronizer implements java.io.Serializable { 
    //等待隊列的頭節點
    private transient volatile Node head;
    //等待隊列的尾節點
    private transient volatile Node tail;
    //同步狀態
    private volatile int state;
    protected final int getState() { return state;}
    protected final void setState(int newState) { state = newState;}
    ...
}
AQS中使用int型變量state來表示同步狀態(0表示空閒,1表示被佔中),通過內置的隊列實現線程的排隊工作。同步狀態state,隊列頭指針head,隊列尾指針tail都使用了volatile進行修飾,保證了多線程之間的可見性。

內部隊列的定義:

static final class Node {
        static final Node SHARED = new Node();
        static final Node EXCLUSIVE = null;
        static final int CANCELLED =  1;
        static final int SIGNAL    = -1;
        static final int CONDITION = -2;
        static final int PROPAGATE = -3;
        volatile int waitStatus;
        volatile Node prev;
        volatile Node next;
        volatile Thread thread;
        Node nextWaiter;
        ...
    }

隊列中的每個節點存儲了前綴節點prev,後綴節點next,節點中的線程thread,節點的狀態waitStatus,狀態的定義有四種,CANCELLED取消狀態,SIGNAL等待觸發狀態,CONDITION等待條件狀態,PROPAGATE狀態需要向後傳播。整個隊列中,只有頭節點中的線程是當前執行線程。

在併發的情況中,如果state的值爲0,即狀態爲空閒,那麼多個線程同時執行CAS修改state的值,成功修改state值的線程獲得該鎖。未獲得鎖的線程將生成新的節點,使用CAS的方式向後排隊。當線程排到隊列後面時,並不會馬上插入,而是進行一個自旋操作。

final boolean acquireQueued(final Node node, int arg) {
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    return interrupted;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } catch (Throwable t) {
            cancelAcquire(node);
            throw t;
        }
    }

因爲在node的排隊的過程,頭節點中的線程(即之前在執行的線程)可能已經執行完成,所以要判斷該node的前一個節點pred是否爲head節點(代表正在執行線程),如果pred == head,表明當前節點是隊列中第一個“有效的”節點,因此再次嘗試tryAcquire獲取鎖,如果獲取到了鎖,就直接將該線程設置到頭節點,否則,再進行判斷:

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.
             */
            pred.compareAndSetWaitStatus(ws, Node.SIGNAL);
        }
        return false;
    }
如果前綴節點的狀態爲SIGNAL,表示這個節點已經設置了鎖一釋放就通知它的狀態,那麼我們就可以安全的將線程插在它的後面,並返回true。如果前綴節點的狀態是取消狀態,那麼就跳過前綴節點,找到前面沒有被取消的第一個節點,插在其後面。否則,前綴節點的狀態要麼是0,要麼是狀態向後發散,我們直接將當前節點的狀態設置爲SIGNAL。以上兩種情況,都無須向隊列中插入,所以均返回false。

線程每次被喚醒時,都要進行中斷檢測,如果發現當前線程被中斷,那麼拋出InterruptedException並退出循環。從無限循環的代碼可以看出,並不是被喚醒的線程一定能獲得鎖,必須調用tryAccquire重新競爭,因爲鎖是非公平的,有可能被新加入的線程獲得,從而導致剛被喚醒的線程再次被阻塞,這個細節充分體現了“非公平”的精髓。

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)
            node.compareAndSetWaitStatus(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 p = tail; p != node && p != null; p = p.prev)
                if (p.waitStatus <= 0)
                    s = p;
        }
        if (s != null)
            LockSupport.unpark(s.thread);
    }

線程釋放鎖的過程:

如果節點的狀態小於0.將節點的狀態設爲0。然後找到下面狀態值小於0的線程,將其喚醒。

總結:

Doug Lea大神寫的代碼相當牛,只看源碼,不看應用,很難完全理解透徹,但源碼看下來總體思路還是很清晰的。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章