面試必考AQS-共享鎖的申請與釋放,傳播狀態

引子

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

共享鎖的申請

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

public final void acquireShared(int arg) {...} // 獲取共享鎖的入口
# protected int tryAcquireShared(int arg); // 嘗試獲取共享鎖
private void doAcquireShared(int arg) {...} // AQS中獲取共享鎖流程整合
private Node addWaiter(Node mode){...} // 將node加入到同步隊列的尾部
# protected int tryAcquireShared(int arg); // 嘗試獲取共享鎖
private void setHeadAndPropagate(Node node, int propagate) {...} // 設置 同步隊列的head節點,以及觸發"傳播"操作
# private void doReleaseShared() {...} // 遍歷同步隊列,調整節點狀態,喚醒待申請節點
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {...} // 如果獲取鎖失敗,則整理隊中節點狀態,並判斷是否要將線程掛起
private final boolean parkAndCheckInterrupt() {...} // 將線程掛起,並在掛起被喚醒後檢查是否要中斷線程(返回是否中斷)
private void cancelAcquire(Node node) {...} // 取消當前節點獲取排它鎖,將其從同步隊列中移除

從入口方法acquireShared()開始分析:

public final void acquireShared(int arg) {
    if (tryAcquireShared(arg) < 0)
        doAcquireShared(arg);
}

就兩步,先讓子類嘗試獲取共享鎖,如果結果小於0,進入AQS邏輯。小於0應該就是獲取鎖失敗,這裏要注意,共享鎖只有在當前存在排它鎖時,纔會申請失敗,那麼就要進入同步隊列再找機會申請鎖了。

private void doAcquireShared(int arg) {
	final Node node = addWaiter(Node.SHARED); // addWaiter()前文有介紹,這裏不展開,注意這裏是SHARED類型的節點
	boolean failed = true; // 同樣一個狀態標識,goto t1
	try {
		boolean interrupted = false;
		for (;;) {
			final Node p = node.predecessor(); // 獲取前置節點
			if (p == head) {  // 如果爲head節點
				int r = tryAcquireShared(arg); // 嘗試申請共享鎖
				if (r >= 0) { // 申請成功
					setHeadAndPropagate(node, r); // goto t2
					p.next = null; // help GC
					if (interrupted)
						selfInterrupt();
					failed = false;
					return;
				}
			}
			if (shouldParkAfterFailedAcquire(p, node) &&
				parkAndCheckInterrupt())
				interrupted = true;
		}
	} finally {
		if (failed) // t1,是否操作取消節點
			cancelAcquire(node);
	}
}

// t2,設置節點nodde爲head
private void setHeadAndPropagate(Node node, int propagate) {
	Node h = head; // Record old head for check below
	setHead(node);
	// 這部分找時間詳細分析
	if (propagate > 0 || h == null || h.waitStatus < 0 ||
		(h = head) == null || h.waitStatus < 0) {
		Node s = node.next;
		if (s == null || s.isShared())
			doReleaseShared();
	}
}

可以看到doAcquireShared()中的流程,與申請同步鎖時的流程基本一致。值得注意的是,共享鎖是允許在同一時刻多個線程同時持有的。而這個處理在AQS中並沒有涉及到,因爲AQS只負責同步隊列的處理以達到同步的目的,因此應該是在子類中實現的。

共享鎖的釋放

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

public final boolean releaseShared(int arg) {...} // 釋放共享鎖的入口
# protected boolean tryReleaseShared(int arg); // 嘗試釋放共享鎖
private void doReleaseShared() {...} // 遍歷同步隊列,調整節點狀態,喚醒待申請節點

還是從入口方法分析:

public final boolean releaseShared(int arg) {
	if (tryReleaseShared(arg)) {
		doReleaseShared();
		return true;
	}
	return false;
}

套路一樣,先由子類嘗試釋放鎖,如果成功了執行doReleaseShared();否則按釋放失敗處理。

private void doReleaseShared() {
    for (;;) { // 循環
        Node h = head;
        if (h != null && h != tail) { // 如果head節點不爲空並且不等於尾結點tail
            int ws = h.waitStatus;
	    if (ws == Node.SIGNAL) { // 如果狀態爲SIGNAL,說明有後繼節點等待被喚醒
	        if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)) // 將head節點狀態重置爲0
		    continue; // 如果重置失敗,for(;;)再次嘗試;t3
		unparkSuccessor(h); // 喚醒h的後繼節點,前文有分析
	    }
            else if (ws == 0 &&
			!compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) // 如果head狀態已經爲0,並且設置PROPAGATE失敗,繼續for(;;) t4
		continue;  // loop on failed CAS
	    }
            if (h == head) // loop if head changed
		break;
    }
}

以上就是釋放共享鎖的流程,大致流程比較簡單。

但注意,t3和t4位置,爲何會CAS操作失敗,只可能是有其他操作已經改變了節點狀態,有可能head節點已經變化了,再次進行for(;;)校驗狀態及處理。

整個for(;;)結束的方式 ,只能是h==head(在執行整段邏輯後,head節點沒有易主),這也是前面遇到各種CAS操作失敗後,必須重新進入for(;;)的原因。

以上都是點心,下面纔是主菜!

傳播狀態(PROPAGATE)

我們在t4位置,遇到了一個節點狀態 PROPAGATE(傳播),而上文在申請共享鎖時,在某種條件下調用了doReleaseShared()方法,間接的也執行了t4位置的代碼,爲何申請鎖也要調用釋放鎖的邏輯?doReleaseShared()方法有什麼特別?

首先理解,爲何申請鎖的流程也要調用釋放鎖的邏輯:

在doReleaseShared()方法中,當head節點狀態等於SIGNAL時,會喚醒後繼節點,在釋放共享鎖場景下,前面釋放後面喚醒很好理解,要注意的是,共享鎖的特點是可以多個線程同時持有的。那麼當一個線程申請共享鎖成功後,必然是可以告知其他等待申請的線程,可以去申請了,那麼主動調用一次doReleaseShared()很合理,也能讓同步隊列中的等待的節點儘快申請到鎖。

doReleaseShared()方法有什麼特別:

在doReleaseShared()方法中,涉及到PROPAGATE狀態的設置compareAndSetWaitStatus(h, 0, Node.PROPAGATE);

在申請共享鎖成功時,調用setHeadAndPropagate(Node node, int propagate)方法,如果滿足if (s == null || s.isShared()),就會導致調用了在doReleaseShared(),也就間接設置了PROPAGATE狀態。

PROPAGATE狀態有什麼特別:

在AQS中搜索PROPAGATE,一共涉及三個方法,分別是:doReleaseShared()、setHeadAndPropagate()以及shouldParkAfterFailedAcquire()。前兩個方法已經介紹過了,最終調用的是doReleaseShared()中的compareAndSetWaitStatus(h, 0, Node.PROPAGATE),目的是將節點狀態變更爲PROPAGATE。

別急,再看一下doReleaseShared()方法:

private void doReleaseShared() {
	for (;;) { // 循環
		Node h = head;
		if (h != null && h != tail) {
			int ws = h.waitStatus;
			if (ws == Node.SIGNAL) {
				if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
					continue;
				unparkSuccessor(h);
			}
			else if (ws == 0 &&
					 !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
				continue;
		}
		if (h == head)
			break;
	}
}

這裏要明確三點:

  1. unparkSuccessor()方法的執行條件是ws==SIGNAL
  2. compareAndSetWaitStatus(h, 0, Node.PROPAGATE)方法的執行條件是ws==0
  3. 很關鍵的代碼是if (h == head)break; 只有head沒有發生變化,循環纔會結束。

我們不要忘了,unparkSuccessor()的作用是讓後繼節點去申請鎖,那麼一旦申請成功,head就會發生變化;但是當這種情況下新的head產生後,又沒有新node入隊,那麼新的head的狀態ws==0就是成立的。

綜上可得出一個臨界狀態,舊的head釋放鎖時,觸發後繼節點申請鎖,並且申請成功,成爲了新的head',並且此刻head'沒有後繼節點,head'==tail,此時可以執行到第2步。

那麼!compareAndSetWaitStatus(h, 0, Node.PROPAGATE)成立的條件又是什麼?自然是head'的狀態ws!=0,什麼時候不等於0,當然是又有新節點入隊,改變ws爲SIGNAL。

這樣就會執行continue;繼續下一次循環。而此時的head'狀態又滿足第1步,也就是會去喚醒後繼節點。

是不是很暈,其實目的只有一個,找出一切可能的情況,去通知後繼節點幹活,去最大可能的嘗試獲取鎖

再結合上面設置PROPAGATE狀態的位置,一是當申請到了共享鎖,需要喚醒後面節點同樣去申請;二是釋放了鎖,需要喚醒後面節點去申請。沒錯,的確是爲了喚醒後繼節點讓它申請鎖。

這就會產生一個問題,同一時刻,可能會有多個線程同時調用了doReleaseShared()方法,但是沒關係doReleaseShared()方法中採用CAS操作去改變節點狀態,不會有問題。

我們來看看另一個涉及到PROPAGATE狀態的方法shouldParkAfterFailedAcquire()中做了哪些事情:

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); // t5
	}
	return false;
}

在t5位置,看到將節點狀態變更爲SIGNAL,結合上面代碼可知,節點狀態ws,

  1. 如果爲SIGNAL,則直接返回true
  2. 如果爲CANCEL,則清理同步隊列
  3. 剩下狀態爲0或PROPAGATE(CONDITION狀態不在這裏考慮),那麼就是將0或PROPAGATE的狀態設置爲SIGNAL

前面doReleaseShared()->unparkSuccessor()的流程以及讓節點狀態ws==SIGNAL了,那麼情況1的思路就清晰了:被喚醒的節點再次嘗試申請鎖失敗後,經歷ws==SIGNAL狀態判斷後,繼續掛起。

但是情況3是在何種場景下被執行呢?

回看:

else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))

前面分析,ws==0和!compareAndSetWaitStatus(h, 0, Node.PROPAGATE) 是兩個矛盾的場景,只有當判斷完ws==0後,立即有節點入隊,纔會導致後面compareAndSetWaitStatus(h, 0, Node.PROPAGATE)執行失敗,但如果執行成功,就是說明沒有新節點入隊,這就會導致head節點的狀態由0變更爲PROPAGATE。

我們有提到過:

同一時刻,可能會有多個線程同時調用了doReleaseShared()方法

那麼,在這個前提下,可能有一種情況是,一個線程在操作head釋放鎖,並執行compareAndSetWaitStatus(h, 0, Node.PROPAGATE) 成功了,節點狀態變爲了PROPAGATE;另一個線程在申請鎖,並且pred==head,將會導致 pred.waitStatus==PROPAGATE,那麼compareAndSetWaitStatus(pred, ws, Node.SIGNAL); 的含義就明白了,申請鎖的線程申請失敗,需要再次被掛起,那麼需要恢復前置節點pred的狀態爲SIGNAL,標記爲存在後繼節點等待被喚醒!

這就是傳播狀態存在的意義。關鍵點臨界狀態的判定,以及最大限度的喚醒後繼節點去申請鎖。

尾聲

在AQS中共享鎖的申請與釋放流程不難理解,難點在於共享鎖申請與釋放期間,“傳播狀態”存在的意義。爲何要在節點狀態中加入“傳播”這個狀態;在分析完AQS源碼後,要全局的去理解它,分析每一個模塊存在的意義,因爲其中沒有任何一行冗餘代碼。

推薦閱讀:

面試必考AQS-AQS概覽

面試必考AQS-AQS源碼全局分析

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

面試必考AQS-共享鎖申請、釋放及傳播狀態

面試必考AQS-await和signal的實現原理

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