面試必考AQS-排它鎖的申請與釋放

引子

前文《面試必考AQS-排它鎖的申請與釋放》已經對AQS中對於排它鎖的申請與釋放流程進行了總結。而對於申請與釋放,在AQS中提現的是與鎖並無關係,而是針對同步隊列的操作,向同步隊列中添加、移除Node實例對象,操作Node中的線程對象。而我們日常使用的鎖類,只是表象,何時可以加鎖、解鎖,達到何種條件纔算加鎖成功、解鎖成功,這纔是AQS鎖實現的功能。接下來將從源碼層面看一下,排它鎖的申請與釋放在AQS中具體是怎樣實現的。

排它鎖的申請

再看一次申請流程的大致方法調用:

public final void acquire(int arg){...} // 獲取排它鎖的入口
# protected boolean tryAcquire(int arg); // 嘗試直接獲取鎖
final boolean acquireQueued(final Node node, int arg) {...} // AQS中獲取排它鎖流程整合
private Node addWaiter(Node mode){...} // 將node加入到同步隊列的尾部
# protected boolean tryAcquire(int arg); // 如果當前node的前置節點pre變爲了head節點,則嘗試獲取鎖(因爲head可能正在釋放)
private void setHead(Node node) {...} // 設置 同步隊列的head節點
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {...} // 如果獲取鎖失敗,則整理隊中節點狀態,並判斷是否要將線程掛起
private final boolean parkAndCheckInterrupt() {...} // 將線程掛起,並在掛起被喚醒後檢查是否要中斷線程(返回是否中斷)
private void cancelAcquire(Node node) {...} // 取消當前節點獲取排它鎖,將其從同步隊列中移除
static void selfInterrupt() {...} // 操作將當前線程中斷

先看一下入口方法acquire(int arg)的代碼。

    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

源碼上是先調用tryAcquire()方法,如果返回false,則進入acquireQueued();由於tryAcquire()是由子類實現的,我們暫時先不看,留到以後梳理AQS實現類時再串講。

那麼從tryAcquire()從方法名來講就是“嘗試獲取鎖”,先假設return=false,也就是“嘗試獲取鎖失敗”,繼續執行調用acquireQueued()。

在進入acquireQueued()之前,會先調用一次addWaiter(),傳入了一個Node.EXCLUSIVE對象;Node.EXCLUSIVE在前文有介紹,是Node類中一個實例變量成員,用於標記排他類型的Node實例。

那麼看一下addWaiter()方法,直接看一下方法內註釋吧。

    private Node addWaiter(Node mode) {
        Node node = new Node(Thread.currentThread(), mode); // 用入參mode=Node.EXCLUSIVE,創建了一個Node實例。
        // 將新創建的節點 加入 同步隊列的尾部
        Node pred = tail; // 拿到同步隊列尾指針
        if (pred != null) {
            node.prev = pred; // 追加到尾部
            if (compareAndSetTail(pred, node)) { // cas方式,更新尾指針指向 新節點node
                pred.next = node; 
                return node;
            }
        }
		// 如果上述方式加入隊列失敗,進入enq(node)方法。
        enq(node);
        return node;
    }
	
    private Node enq(final Node node) {
        for (;;) { // 無限嘗試
            Node t = tail;
            if (t == null) { 
				// 尾結點不能指向爲null,說明隊列爲null,需要初始化同步隊列的頭尾指針
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
				// 這段代碼的含義,與addWaiter()中的一致,也就是不停嘗試將節點加入到同步隊列
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

那麼addWaiter()方法就一個目的:將新的node加入到同步隊列的尾部,並將node返回。

接下來繼續調用acquireQueued()方法,我們看看它做了什麼:

    final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;// 一個狀態標記,goto t1
        try {
            boolean interrupted = false; // 一個狀態標記,goto t2
            for (;;) {
                final Node p = node.predecessor(); // 獲取node的前置節點p,前文有介紹過
                if (p == head && tryAcquire(arg)) {  // 如果p是頭節點,立即嘗試獲取鎖(注意只有隊列head的下一個節點才能去獲取鎖)
                    setHead(node); // 如果返回true,說明獲取鎖成功,設置head爲當前節點(說明原有head已經釋放鎖)
                    p.next = null; // help GC
                    failed = false; 
                    return interrupted; // t2',將中斷標識返回
                }
                if (shouldParkAfterFailedAcquire(p, node) &&  
                    parkAndCheckInterrupt())
                    interrupted = true; // t2,是否中斷線程的標記,goto t2'
            }
        } finally {
            if (failed) // t1,狀態標識的值,決定是否調用cancelAcquire()
                cancelAcquire(node);
        }
    }

流程比較清晰,一個for(;;),循環嘗試獲取鎖,如果節點到達同步隊列的head之後,則嘗試申請鎖,申請成功返回,否則調用shouldParkAfterFailedAcquire()和parkAndCheckInterrupt(),我們繼續往下看。

    // 入參:pred 就是當前節點的前置節點,node就是當前節點
    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus; // 獲取前置節點的狀態
        if (ws == Node.SIGNAL) // SIGNAL 代表 節點等待被喚醒,可以理解爲是一種阻塞狀態的標記
            /*
             * pred節點爲SIGNAL狀態,可以認爲node允許阻塞
             */
            return true;
        if (ws > 0) { 
	    // 由於ws的狀態只有CANCEL是大於0,因此節點無效,前置節點可以從隊列移除            
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0); // 遍歷同步隊列中,node前面的所有節點,將CANCEL狀態的都移除
            pred.next = node;
        } else {
            // 如果前置節點狀態 小於0,說明是正常節點(但不是SIGNAL),
            // 先cas設置爲SIGNAL(標記存在後置節點),然後交給外層for(;;)再次嘗試申請鎖
	    // else中不返回true的目的是:
            //    防止過早進入parkAndCheckInterrupt(),而應該再次嘗試申請鎖(臨近狀態控制)
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

如果shouldParkAfterFailedAcquire()返回true,則進入parkAndCheckInterrupt()方法,這裏只做了一件事,將線程掛起。

    private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this); // 掛起當前線程;由此線程等待被喚醒
	// other thread going to work..
        return Thread.interrupted(); // 當線程被喚醒後,返回線程是否中斷
    }

我們再回到acquireQueued()方法看一下,注意看註釋:

    final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
		// 這裏的工作是:檢查前置節點狀態,如果是SIGNAL,則讓當前node代表的線程掛起
		// 如果不是SIGNAL,則會繼續for(;;)努力申請鎖。
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt()) 
		    // 如果parkAndCheckInterrupt()==true說明線程被中斷,需要設置interrupted標記
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

再看看cancelAcquire()內部實現:

    private void cancelAcquire(Node node) {
        // Ignore if node doesn't exist
        if (node == null)
            return;
		
	// 節點操作取消申請,清空線程對象
        node.thread = null;

        // 將同步隊列中node前的節點,移出隊列
        Node pred = node.prev;
        while (pred.waitStatus > 0)
            node.prev = pred = pred.prev;

        // preNext 是清理後的同步隊列的pre節點的下一個有效節點
        Node predNext = pred.next;

        // Can use unconditional write instead of CAS here.
        // After this atomic step, other Nodes can skip past us.
        // Before, we are free of interference from other threads.
        node.waitStatus = Node.CANCELLED;

        // 如果node在隊尾,則將隊尾tail指向pred節點
        if (node == tail && compareAndSetTail(node, pred)) {
            compareAndSetNext(pred, predNext, null);// 並將pred節點的next指向predNext。
	    // 以上都是在整理同步隊列節點;node在隊尾,直接丟棄即可
        } else {
            // If successor needs signal, try to set pred's next-link
            // so it will get one. Otherwise wake it up to propagate.
            int ws;
            if (pred != head &&
                ((ws = pred.waitStatus) == Node.SIGNAL ||
                 (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
                pred.thread != null) {
		// t3 能進入這裏的條件是:
		// 1、前置節點不是head節點
		// 並且
		// 2、前置節點狀態是SIGNAL 或者 (前置節點狀態小於0 並且 可以修改爲SIGNAL)
		// 並且
		// 3、前置節點的線程不爲null
		// 以上,都是在判定 pred節點是否爲一個有效節點
                Node next = node.next;
                if (next != null && next.waitStatus <= 0)
                    compareAndSetNext(pred, predNext, next);
		    // 將前置節點的next指向當前node的next,從同步隊列中移除node
            } else {
		// 如果t3 的條件未達到,
		// 則手動喚醒node代表的線程
                //     (假設node還在被掛起,則可以交給acquireQueued()中的for(;;) 重新搞一波)
                unparkSuccessor(node); 
            }
            node.next = node; // help GC
        }
    }

以上,就是申請排它鎖的全部流程,其中比較關鍵的方法,都是涉及到waitStatus的值變化的方法,如shouldParkAfterFailedAcquire()、parkAndCheckInterrupt()、cancelAcquire(),思路要理清才能搞明白。

重新回憶下申請鎖都做了什麼:

1、先交給子類去嘗試申請鎖,如果申請成功了,則結束流程;
2、否則將申請線程包裝爲node節點,並加入同步隊列
3、循環檢查當前節點的前置節點是否爲head,如果是則申請獲取鎖
4、否則檢查節點狀態,並將線程掛起,等待前面有節點釋放鎖後,來主動喚醒該線程
5、將CANCEL狀態的節點清理掉。

鎖既然已經申請到了,那麼不可避免的就要釋放;接下來再分析一下釋放鎖的源碼。

排它鎖的釋放

再看一次釋放流程的大致方法調用:

public final boolean release(int arg) {...} // 釋放排它鎖的入口
# protected boolean tryRelease(int arg); // 嘗試直接釋放鎖
private void unparkSuccessor(Node node) {...} // 喚醒後繼節點

先看入口方法release()的工作:

    public final boolean release(int arg) {
        if (tryRelease(arg)) { // 同樣先讓子類執行tryRelease()來嘗試釋放鎖,
	    // 這回是如果成功了繼續
            Node h = head;
            if (h != null && h.waitStatus != 0)
		// head如果不爲null並且狀態不是0,則調用unparkSuccessor()
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

釋放鎖具體怎麼釋放,交給了子類去操作,那麼說明一點,tryRelease()中並不涉及同步隊列的操作。而unparkSuccessor(h);中做的事情,纔是AQS的關鍵。

	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);
	    // 修改node節點狀態爲SIGNAL

        /*
         * 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) { // head的下一個節點,如果爲CANCEL狀態
            s = null;
            // 清理同步隊列,注意這裏是從尾部開始向前清理的
            for (Node t = tail; t != null && t != node; t = t.prev)
                if (t.waitStatus <= 0)
                    s = t;
        }
	// 由於是從尾部清理,因此s最後的值應該是同步隊列中第一個waitState<=0的節點
	// 那麼unpark()操作的就是同步隊列中第一個等待被喚醒的節點
        if (s != null)
            LockSupport.unpark(s.thread);
    }

以上就是釋放鎖的流程;重新回憶一下釋放鎖到底幹了什麼。

1、先交給子類去嘗試釋放鎖,如果成功則處理同步隊列的節點
2、將同步隊列中的無效節點移除,然後將隊列中node之後第一個有效節點喚醒

其實就這麼簡單,但是代碼很精妙,尤其是從尾部遍歷同步隊列的思路。

尾聲

對於AQS中排它鎖的申請與釋放,思路是比較清晰的,而且行爲方向單一,就是針對同步隊列的入隊與出隊操作,以及在某些情況下針對同步隊列進行遍歷;再加上LockSupport的park和unpark調用,來控制線程狀態。

這就是AQS的一部分,仔細分析源碼,其實沒那麼複雜。

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