AQS深入理解系列(三)共享鎖的獲取與釋放

前言

在前面兩篇系列文章中,已經講解了獨佔鎖的獲取和釋放過程,而共享鎖的獲取與釋放過程也很類似,如果你前面獨佔鎖的內容都看懂了,那麼共享鎖你也就觸類旁通了。

JUC框架 系列文章目錄

共享鎖與獨佔鎖的區別

共享鎖與獨佔鎖最大的區別在於,共享鎖的函數名裏面都帶有一個Shared(抖個機靈,當然不是這個)。

  • 獨佔鎖是線程獨佔的,同一時刻只有一個線程能擁有獨佔鎖,AQS裏將這個線程放置到exclusiveOwnerThread成員上去。
  • 共享鎖是線程共享的,同一時刻能有多個線程擁有共享鎖,但AQS裏並沒有用來存儲獲得共享鎖的多個線程的成員。
  • 如果一個線程剛獲取了共享鎖,那麼在其之後等待的線程也很有可能能夠獲取到鎖。但獨佔鎖不會這樣做,因爲鎖是獨佔的。
  • 當然,如果一個線程剛釋放了鎖,不管是獨佔鎖還是共享鎖,都需要喚醒在後面等待的線程。

讓我們把共享鎖與獨佔鎖的函數名都列出來看一下:

獨佔鎖 共享鎖
tryAcquire(int arg) tryAcquireShared(int arg)
tryAcquireNanos(int arg, long nanosTimeout) tryAcquireSharedNanos(int arg, long nanosTimeout)
acquire(int arg) acquireShared(int arg)
acquireQueued(final Node node, int arg) doAcquireShared(int arg)
acquireInterruptibly(int arg) acquireSharedInterruptibly(int arg)
doAcquireInterruptibly(int arg) doAcquireSharedInterruptibly(int arg)
doAcquireNanos(int arg, long nanosTimeout) doAcquireSharedNanos(int arg, long nanosTimeout)
release(int arg) releaseShared(int arg)
tryRelease(int arg) tryReleaseShared(int arg)
- doReleaseShared()

從上表可以看到,共享鎖的函數是和獨佔鎖是一一對應的,而且大部分只是函數名加了個Shared,從邏輯上看也是很相近的。

doReleaseShared沒有對應到獨佔鎖的方法是因爲它的邏輯是包含了unparkSuccessor,是建立在unparkSuccessor之上的,你可以簡單地認爲,doReleaseShared對應到獨佔鎖的方法是unparkSuccessor。最主要的是,它們的使用時機不同:

  • 在獨佔鎖中,釋放鎖時,會調用unparkSuccessor
  • 在共享鎖中,獲得鎖和釋放鎖時,都會調用到doReleaseShared。不過獲得共享鎖時,是在一定條件下調用doReleaseShared

觀察Semaphore的內部類

爲了看到AQS的子類實現部分,我們從Semaphore看起。

    abstract static class Sync extends AbstractQueuedSynchronizer {
        Sync(int permits) {
            setState(permits);
        }

        final int nonfairTryAcquireShared(int acquires) {
            for (;;) {
                int available = getState();
                int remaining = available - acquires;
                if (remaining < 0 ||
                    compareAndSetState(available, remaining))
                    return remaining;
            }
        }
	}

    static final class NonfairSync extends Sync {
        protected int tryAcquireShared(int acquires) {
            return nonfairTryAcquireShared(acquires);
        }
    }

    static final class FairSync extends Sync {
        protected int tryAcquireShared(int acquires) {
            for (;;) {
                if (hasQueuedPredecessors())
                    return -1;
                int available = getState();
                int remaining = available - acquires;
                if (remaining < 0 ||
                    compareAndSetState(available, remaining))
                    return remaining;
            }
        }
    }
  • 首先看到Sync的構造器,看來參數permits是代表共享鎖的數量。
  • 觀察tryAcquireShared的公平和非公平鎖的邏輯,發現區別只是 公平鎖裏面每次循環都會判斷hasQueuedPredecessors()的返回值。

這裏先給大家講一下tryAcquireShared
參數acquires代表這次想要獲得的共享鎖的數量是多少。
返回值則有三種情況:

  1. 如果返回值大於0,說明獲取共享鎖成功,並且後續獲取也可能獲取成功。
  2. 如果返回值等於0,說明獲取共享鎖成功,但後續獲取可能不會成功。
  3. 如果返回值小於0,說明獲取共享鎖失敗。

直接看公平版本的tryAcquireShared,上面返回的地方:

  • hasQueuedPredecessors()如果返回了true,說明有線程排在了當前線程之前,現在公平版本又不能插隊,所以結束返回-1,代表獲取失敗。
  • 如果remaining < 0成立,說明想要獲取的共享鎖數量已經超過了當前已有的數量,那麼直接返回一個負數remaining,代表獲取失敗。
  • 如果remaining < 0不成立,說明想要獲取的共享鎖數量沒有超過了當前已有的數量(等於0代表將會獲取剩餘所有的共享鎖)。且接下來如果compareAndSetState(available, remaining)成功,那麼返回一個>=0的數remaining,代表獲取成功。

接下來我們談談共享鎖的tryAcquireShared和獨佔鎖的tryAcquire的不同之處:

  • tryAcquire的返回值是boolean型,它只代表兩種狀態(獲取成功或失敗)。而tryAcquireShared的返回值是int型,如上有三種情況。
  • tryAcquireShared使用了自旋(死循環),但tryAcquire沒有自旋。這將導致tryAcquire最多執行一次CAS操作修改同步器狀態,但tryAcquireShared可能有多次。tryAcquireShared具體地講,只要remaining>=0的(remaining < 0不成立),就一定會去嘗試CAS設置同步器的狀態。使用自旋的原因想必是,鎖是共享的,既然還可能獲取到(remaining>=0的),就一定要去嘗試。
        protected final boolean tryReleaseShared(int releases) {
            for (;;) {
                int current = getState();
                int next = current + releases;
                if (next < current) // overflow
                    throw new Error("Maximum permit count exceeded");
                if (compareAndSetState(current, next))
                    return true;
            }
        }

最後再看tryReleaseShared的實現,也用到了自旋操作,因爲完全有可能多個線程同時釋放共享鎖,同時調用tryReleaseShared,所以需要用自旋保證 共享鎖的釋放最終能體現到同步器的狀態上去。另外,除非int型溢出,那麼此函數只可能返回true。

共享鎖的獲取

上面講完了Semaphore的內部類,接下來我們就可以盡情地在AQS的源碼裏暢遊了。

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

acquireShared對應到獨佔鎖的方法是acquire

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

咋一看感覺差別有點大,其實我們被迷惑了,後面我們會發現,之所以acquireShared裏沒有顯式調用addWaiterselfInterrupt,是因爲這兩件事都被放到了doAcquireShared(arg)的邏輯裏面了。

接下來看看doAcquireShared方法的邏輯,它對應到獨佔鎖是acquireQueued,除了上面提到的兩件事,它們其實差別很少:

    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) {  //前驅是head時,才嘗試獲得共享鎖
                    int r = tryAcquireShared(arg);
                    if (r >= 0) {  //獲取共享鎖成功時,才進行善後操作
                        setHeadAndPropagate(node, r);  //獨佔鎖這裏調用的是setHead
                        p.next = null; 
                        if (interrupted)
                            selfInterrupt(); //這件事也放到裏面來了
                        failed = false;
                        return;
                    }
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

acquireQueued在獲得獨佔鎖成功時,執行的是:

if (p == head && tryAcquire(arg)) {  // tryAcquire返回true,代表獲取獨佔鎖成功
    setHead(node);
    p.next = null; 
    failed = false;
    return interrupted;
}

所以對比發現,共享鎖的doAcquireShared有兩處不同:

  1. 創建的節點不同。共享鎖使用addWaiter(Node.SHARED),所以會創建出想要獲取共享鎖的節點。而獨佔鎖使用addWaiter(Node.EXCLUSIVE)
  2. 獲取鎖成功後的善後操作不同。共享鎖使用setHeadAndPropagate(node, r),因爲剛獲取共享鎖成功後,後面的線程也有可能成功獲取,所以需要在一定條件喚醒head後繼。而獨佔鎖使用setHead(node)
    private void setHeadAndPropagate(Node node, int propagate) {
        Node h = head; 
        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();
        }
    }

    private void setHead(Node node) {
        head = node;
        node.thread = null;
        node.prev = null;
    }

setHead函數只是將剛成爲將成爲head的節點變成一個dummy node。而setHeadAndPropagate裏也會調用setHead函數。但是它在一定條件下還可能會調用doReleaseShared,看來這就是單詞Propagate的由來了,也就是我們一直說的“如果一個線程剛獲取了共享鎖,那麼在其之後等待的線程也很有可能能夠獲取到鎖”。

doReleaseShared留到之後講解,因爲共享鎖的釋放也會用到它。

關於setHeadAndPropagate的詳解請看這篇setHeadAndPropagate源碼分析,主要有兩張圖幫助大家理解setHeadAndPropagate裏的這個超長的if判斷。

共享鎖的釋放

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

releaseShared對應到獨佔鎖的方法是release

public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

可見獨佔鎖的邏輯比較簡單,只是在head狀態不爲0時,就喚醒head後繼。

而共享鎖的邏輯則直接調用了doReleaseShared,但在獲取共享鎖成功時,也可能會調用到doReleaseShared。也就是說,獲取共享鎖的線程(分爲:已經獲取到的線程 即執行setHeadAndPropagate中、等待獲取中的線程 即阻塞在shouldParkAfterFailedAcquire裏)和釋放共享鎖的線程 可能在同時執行這個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;            // loop to recheck cases
                unparkSuccessor(h);
            }
            else if (ws == 0 &&
                     !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                continue;                // loop on failed CAS
        }
        if (h == head)                   // loop if head changed
            break;
    }
}

我們來仔細分析下這個函數的邏輯:

  • 邏輯是一個死循環,每次循環中重新讀取一次head,然後保存在局部變量h中,再配合if(h == head) break;,這樣,循環檢測到head沒有變化時就會退出循環。注意,head變化一定是因爲:acquire thread被喚醒,之後它成功獲取鎖,然後setHead設置了新head。而且注意,只有通過if(h == head) break;即head不變才能退出循環,不然會執行多次循環。
  • if (h != null && h != tail)判斷隊列是否至少有兩個node,如果隊列從來沒有初始化過(head爲null),或者head就是tail,那麼中間邏輯直接不走,直接判斷head是否變化了。
  • 如果隊列中有兩個或以上個node,那麼檢查局部變量h的狀態:
    • 如果狀態爲SIGNAL,說明h的後繼是需要被通知的。通過對CAS操作結果取反,將compareAndSetWaitStatus(h, Node.SIGNAL, 0)unparkSuccessor(h)綁定在了一起。說明了只要head成功得從SIGNAL修改爲0,那麼head的後繼的代表線程肯定會被喚醒了。
    • 如果狀態爲0,說明h的後繼所代表的線程已經被喚醒或即將被喚醒,並且這個中間狀態即將消失,要麼由於acquire thread獲取鎖失敗再次設置head爲SIGNAL並再次阻塞,要麼由於acquire thread獲取鎖成功而將自己(head後繼)設置爲新head並且只要head後繼不是隊尾,那麼新head肯定爲SIGNAL。所以設置這種中間狀態的head的status爲PROPAGATE,讓其status又變成負數,這樣可能被被喚醒線程檢測到。
    • 如果狀態爲PROPAGATE,直接判斷head是否變化。
  • 兩個continue保證了進入那兩個分支後,只有當CAS操作成功後,纔可能去執行if(h == head) break;,纔可能退出循環。
  • if(h == head) break;保證了,只要在某個循環的過程中有線程剛獲取了鎖且設置了新head,就會再次循環。目的當然是爲了再次執行unparkSuccessor(h),即喚醒隊列中第一個等待的線程。

head狀態爲0的情況

  • 如果等待隊列中只有一個dummy node(它的狀態爲0),那麼head也是tail,且head的狀態爲0。
  • 等待隊列中當前只有一個dummy node(它的狀態爲0),acquire thread獲取鎖失敗了(無論獨佔還是共享),將當前線程包裝成node放到隊列中,此時隊列中有兩個node,但當前線程還沒來得及執行一次shouldParkAfterFailedAcquire
  • 此時隊列中有多個node,有線程剛釋放了鎖,剛執行了unparkSuccessor裏的if (ws < 0) compareAndSetWaitStatus(node, ws, 0);把head的狀態設置爲了0,然後喚醒head後繼線程,head後繼線程獲取鎖成功,直到head後繼線程將自己設置爲AQS的新head的這段時間裏,head的狀態爲0。
    • 具體地講,如果是共享鎖的話,一定是在調用unparkSuccessor之前就把head的狀態變成0了,因爲if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
    • 上面這種情況還可以繼續延伸,在“喚醒head後繼線程”後,head後繼線程喚醒後第一次循環獲取鎖失敗(你可能會疑問,上面的場景明明是剛有人釋放了鎖,爲什麼這裏會失敗,因爲多線程環境下有可能被別的不公平獲取方式插隊了),調用shouldParkAfterFailedAcquire又將head設置回SIGNAL了,然後第二次循環開始之前(假設head後繼線程此時分出去時間片),又有一個釋放鎖的線程在執行doReleaseShared裏面的compareAndSetWaitStatus(h, Node.SIGNAL, 0)成功並且還unpark了處於喚醒狀態的head後繼線程,然後第二次循環開始(假設head後繼線程此時得到時間片),獲取鎖成功。
      • 注意,如果unpark一個已經喚醒的線程,它的副作用是下一次park這個線程,線程不會阻塞。下下次park線程,纔會阻塞。

總結:

  • head狀態爲0的情況,屬於一種中間狀態。
  • 這種中間狀態將變化爲,head狀態爲SIGNAL,不管acquire thread接下來是獲取鎖成功還是失敗。不過獲取鎖成功這種情況,需要考慮head後繼(也就是包裝acquire thread的那個node)不是隊尾,如果是隊尾,那麼新head的狀態也是爲0的了。

同時執行doReleaseShared

這個函數的難點在於,很可能有多個線程同時在同時運行它。比如你創建了一個Semaphore(0),讓N個線程執行acquire(),自然這多個線程都會阻塞在acquire()這裏,然後你讓另一個線程執行release(N)

  • 此時 釋放共享鎖的線程,肯定在執行doReleaseShared。
  • 由於 上面這個線程的unparkSuccessor,head後繼的代表線程也會喚醒,進而執行doReleaseShared。
  • 重複第二步,獲取共享鎖的線程 又會喚醒 新head後繼的代表線程。

觀察上面過程,有的線程 因爲CAS操作失敗,或head變化(主要是因爲這個),會一直退不出循環。進而,可能會有多個線程都在運行該函數。doReleaseShared源碼分析中的圖解舉例了一種循環繼續的例子,當然,循環繼續的情況有很多。

總結

  • 共享鎖與獨佔鎖的最大不同,是共享鎖可以同時被多個線程持有,雖然AQS裏面沒有成功用來保存持有共享鎖的線程們。
  • 由於共享鎖在獲取鎖和釋放鎖時,都需要喚醒head後繼,所以將其邏輯抽取成一個doReleaseShared的邏輯了。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章