併發系列(九)-----AQS詳解共享模式的資源獲取與釋放
一 簡介
到目前為止已經知道了AQS中同步佇列的基本的工作原理可以總結為維護同步佇列,獲取資源和改變執行緒狀態。上一篇文章中主要總結了獨佔模式下的資源獲取。這篇主要總結一下AQS中的共享模式。
共享模式從字面上理解就是,這個資源可以被多個執行緒共享。在獨佔模式下,我們知道state的狀態最初的值是0.如果某個執行緒獲取到資源了state就加了1釋放資源了就減去1。當state變為0的時候喚醒後繼節點的執行緒,讓後繼節點的執行緒去持有資源。那麼好了我們可以不可以這麼幹,一開始我給state設定一個值,當一個執行緒獲取資源後,我的state就減去1,其它在來時我在減去1...以此類推,直到執行緒獲取資源減到為0為止。表示資源沒有了其他執行緒就無法獲取了只能去等待了。這樣的話多個執行緒就將這個state共享了,其實這就是AQS中的共享模式。
二 共享資源的獲取
上面我們已經總出了它的大體邏輯了,可能在實現中還有一些細節需要去理解。下面看共享資源的獲取
/** *共享資源的獲取 * * @param arg 要獲取的資源數 */ public final void acquireShared(int arg) { //獲取共享鎖小於0表示資源沒有了,也就獲取失敗了 if (tryAcquireShared(arg) < 0) { //真正的去獲取資源的方法 doAcquireShared(arg); } }
上面的方法中有關tryAcquireShared()我們就不必看了,看的話也是拋了一個異常,但是可以去看看它子類的實現比如Semaphore的實現,如果小於0表示沒有獲取到資源。看一下沒有獲取到資源會怎麼做,下面是原始碼。
/** * 共享模式的資源競爭 * * @param arg 資源 */ private void doAcquireShared(int arg) { //新增一個共享節點到同步佇列尾部 final Node node = addWaiter(Node.SHARED); boolean failed = true; try { //是否中斷 boolean interrupted = false; for (; ; ) { //獲取當節點的前驅節點 final Node p = node.predecessor(); if (p == head) { //如果前驅節點時頭接節點的話,再次獲取資源r代表的是剩餘資源 int r = tryAcquireShared(arg); if (r >= 0) { //大於0獲取到了就要檢查還有沒有資源,有的話還要去繼續去喚醒下一個節點 setHeadAndPropagate(node, r); p.next = null; if (interrupted) { selfInterrupt(); } failed = false; return; } } if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) { interrupted = true; } } } finally { if (failed) { cancelAcquire(node); } } }
上面的方法和獨佔模式下的整體上沒有太大的區別,但是在共享模式下獲取到資源後加了一個setHeadAndPropagate()這個方法,這個方法是用來判斷資源還有剩餘且下一個節點是否是共享節點的。下面是原始碼
/**
* 設定佇列頭部,並檢查後繼者是否在等待 在共享模式下,
*
* @param node node節點
* @param propagate 剩餘資源數
*/
private void setHeadAndPropagate(Node node, int propagate) {
//獲取頭節點
Node h = head;
//將剛獲取到資源的節點設定為頭節點
setHead(node);
//propagate>0表示還有資源可以獲取
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
Node s = node.next;
//如果當前節點的下一個節點為空,或者下一個節點是共享
//節點的時
if (s == null || s.isShared()) {
//釋放資源
doReleaseShared();
}
}
}
上面這個方法用來將剛獲取到資源的節點設定為頭節點,同時還檢查了有沒有資源和有沒有共享節點。如果有的話釋放資源(注意的釋放資源並沒有操作state只是做了喚醒下一個節點,由下一個節點的執行緒去操作資源)下面是原始碼
/**
*
*
* 喚醒下一個節點
*/
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;
} else {
//喚醒下一個節點
unparkSuccessor(h);
}
//將節點狀態設定為-3傳播狀態
} else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) {
continue;
}
}
//這裡當喚醒執行緒的時候這裡的頭節點head是可能變得,一但獲取成功就變了,而h還是以前頭節點的引用
if (h == head) {
break;
}
}
}
這個方法可能有這樣一個問題,這個迴圈不就迴圈一次嗎?為什麼要用死迴圈呢?其實不是的,h是一個頭節點的引用,這個h可能是一箇舊的頭節點,因為在上一個方法中有設定頭節點的方法,head是獲取資源的執行緒節點,在成功的喚醒了下一個節點而且被喚醒的節點前驅節點又是頭節點,資源有存在且沒有中斷的話是可以獲取成功的獲取資源的,那麼這個head就有可能是被修改了,所以for迴圈才會一直繼續.。才會繼續去拿資源一直拿到沒有為止。
三 共享資源的釋放
在上面的方法中已經介紹了資源的釋放,而且資源的釋放就是操作state和喚醒下一個節點,下面是原始碼的
/**
* 釋放資源
*
* @param arg 要釋放的資源數
* @return 是否釋放成功
*/
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
呼叫tryReleaseShared(int)由子類實現去操作並state,返回boolean內部呼叫doReleaseShared()方法。doReleaseShared()方法在上文已經介紹過了。
四 總結
共享模式的資源獲取與釋放主要在於將資源預先初始化一定數目,然後各個執行緒去獲取特定數量的資源,在獲取資源時如果資源還有剩下就喚醒後繼節點繼續獲取,知道資源沒有剩餘,其他資源只能進入等待狀態。釋放時做了操作資源和喚醒下一個節點的操作。