CountDownLatch
閉鎖可以使一個或多個線程等待一組事件的發生,內部的計數器記錄了事件的數量。兩個主要的方法就是await和countDown。
public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
public void countDown() {
sync.releaseShared(1)
}
可以看到,這兩種均使用了AQS中的共享類型方法。因此在同步器中他也需要實現tryAcquiredShared方法和tryReleaseShared方法。在tryAcquiredShared實現中,如果state=0則表示成功返回1(要等待的事件全部發生),表示后續的acquire也會成功,否則返回-1表示acquire失敗。在tryReleaseShared方法中,獲取state并減1(發生了一個事件),如果減到0則release成功。
然后我們描述一下await的整個過程。首先我們假設CountDownLatch的初始值為n。一個線程調用了await方法,此時同步器調用acquireSharedInterruptibly(1)。在這個方法中,首先會在共享模式下try一下,根據上一段我們的描述,此時state=n顯然不為0,那么try失敗,調用doAcquireSharedInterruptibly。在這個方法里,與其他acquire方法基本類似,都會在同步隊列添加一個節點,然后判斷前驅是否為head以決定能否立即try一次。如果前驅為head,并且try成功了需要調用setHeadAndPropagate(顯然本次不會調用)。然后會把前驅設為SIGNAL并阻塞當前線程。
接下來是countDown過程,表示一個事件發生了。還是一樣的首先都會try一下,獲取state并減1,此時state=n-1.我們先假設state仍舊大于0即還有事件沒有發生,那么tryReleaseShared會返回false。這種情況下releaseShared會直接返回!
那么當state=1的時候又調用了一次releaseShared會發生什么呢。同樣的我們先try一下,這是經過CAS比較我們發現state=0了!然后我們就需要研究一下同步隊列了,如果隊列頭結點是SIGNAL,就表明它的后繼正等待被喚醒,然后我們就解鎖其后繼節點(只解鎖了一個!)。這時,第一個進入同步隊列的線程被喚醒,它將從被阻塞的地方繼續執行,然后調用一次tryAcquireShared,此時會成功,因為state=0了。然后該節點將自己設置為隊列頭(setHeadAndPropagate),并在該方法內調用了一次doReleaseShared,這將導致后續的節點被依次喚醒。
Semaphore
信號量中有兩種模式,即公平和非公平模式。它用于實現控制同時訪問某個資源的操作數量。唯一的區別就是在tryAcquire的時候,公平模式會先看一下所在同步隊列前面有沒有節點在等,如果有則標記自己本次try失敗。
Semaphore的acquire操作會調用acquireSharedInterruptibly方法。此時如果資源數夠,那么CAS設置state。如果資源不夠,就需要進入同步隊列等待了。其執行結構與CountDownLatch并無區別。但要注意的是這里的tryAcquire很好的反映了該方法的定義,如果返回值>0,表示后繼的acquire可能會成功;如果返回值=0,那么本次成功,后繼的acquire不成功;如果<0則失敗。當資源值小于等于0的時候線程就會被park方法阻塞掉。
同樣的release操作會將信號量state遞增,并解鎖一個后繼節點。被喚醒的線程會繼續嘗試tryAcquire,如果遞增后的信號量依舊小于0,那么繼續阻塞。如果大于0,那么try成功,該節點將自己設為頭結點并繼續調用doReleaseShared()方法解鎖后繼節點。
通過這兩種可以發現,利用同步器的工具類需要的就是根據自己的需求實現相關的acquire和release操作。在閉鎖中,acquire成功與否取決于事件數是否為0,而在信號量中則取決于資源數是否大于0.同樣,操作release,閉鎖需要將事件數-1,而信號量需要將事件數+1.這兩種同步工具類均使用了共享模式,因為可能允許多個線程同時獲取鎖。并且release導致的解鎖只會解鎖一個后繼節點,后續節點的解鎖操作將依靠前驅節點的傳播過程。