在複習了ReentrantLock可重入的獨佔鎖後,我們來分析下使用了共享鎖的CountDownLatch。
CountDownLatch使用示例
先看一個CountDownLatch的使用示例。
package com.wangcc.springbootexample.concurrency;
import java.util.Random;
import java.util.concurrent.CountDownLatch;
public class CountDownLatchTest {
public static void main(String[] args) throws InterruptedException {
CountDownLatch latch = new CountDownLatch(4);
for (int i = 0; i < latch.getCount(); i++) {
new Thread(new MyThread(latch), "player" + i).start();
}
System.out.println("正在等待所有玩家準備好");
latch.await();
System.out.println("開始遊戲");
}
private static class MyThread implements Runnable {
private CountDownLatch latch;
public MyThread(CountDownLatch latch) {
this.latch = latch;
}
@Override
public void run() {
try {
Random rand = new Random();
int randomNum = rand.nextInt((3000 - 1000) + 1) + 1000;// 產生1000到3000之間的隨機整數
Thread.sleep(randomNum);
System.out.println(Thread.currentThread().getName() + " 已經準備好了, 所使用的時間爲 " + ((double) randomNum / 1000) + "s");
latch.countDown();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
程序輸出如下:
正在等待所有玩家準備好
player2 已經準備好了, 所使用的時間爲 1.687s
player1 已經準備好了, 所使用的時間爲 1.992s
player0 已經準備好了, 所使用的時間爲 2.095s
player3 已經準備好了, 所使用的時間爲 2.453s
開始遊戲
通過上述示例,我們能夠知道CountDownLatch的用處。當一個線程的操作必須要等待其他幾個線程執行完後才能繼續執行的時候,就可以使用CountDownLatch來實現。在創建CountDownLatch中時,會傳遞一個int類型參數count,該參數是“鎖計數器”的初始狀態,表示該“共享鎖”最多能被count個線程同時獲取。當某線程調用該CountDownLatch對象的await()方法時,該線程會等待“共享鎖”可用時,才能獲取“共享鎖”進而繼續運行。而“共享鎖”可用的條件,就是“鎖計數器”的值爲0!而“鎖計數器”的初始值爲count,每當一個線程調用該CountDownLatch對象的countDown()方法時,纔將“鎖計數器”-1;通過這種方式,必須有count個線程調用countDown()之後,“鎖計數器”才爲0,而前面提到的等待線程才能繼續運行!
CountDownLatch源碼分析
先看構造方法
public CountDownLatch(int count) {
if (count < 0) throw new IllegalArgumentException("count < 0");
this.sync = new Sync(count);
}
CountDownLatch
與ReentrantLock
一樣,有一個內部類,名爲Sync,是AQS子類。CountDownLatch#Sync
調用的是AQS對於共享鎖的實現。在構造方法中,會將鎖計數器的值count通過Sync的構造方法賦給AQS中的state屬性。
Sync(int count) {
setState(count);
}
//AQS
protected final void setState(int newState) {
state = newState;
}
接下來看countDown()
方法
public void countDown() {
sync.releaseShared(1);
}
直接調用AQS中的final方法releaseShared
方法,該方法是嘗試釋放共享鎖的方法,在分析AQS中分析過,是否成功釋放共享鎖,要看子類實現的tryReleaseShared()
方法是否返回true,返回true則代表成功釋放鎖,就會去調用doReleaseShared()
方法去喚醒阻塞線程。我們看下tryReleaseShared()
的實現。
//CountDownLatch#Sync
protected boolean tryReleaseShared(int releases) {
// Decrement count; signal when transition to zero
for (;;) {
int c = getState();
if (c == 0)
return false;
int nextc = c-1;
//CAS將state值置爲nextc
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}
實現非常簡單,當state狀態爲0的時候,也就是當前共享鎖已經釋放了,那麼就返回false。否則將state-1,CAS設置新的state值,去比較state-1
的值是否等於0,如果等於0,那麼就返回true,否則就返回false。通過這個方法,我們知道返回true的時候就是第count個線程調用countDown()
的時候。
最後來看下await()
方法
public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
直接調用了AQS中的final方法acquireSharedInterruptibly()
方法。這個方法的具體實現在AQS中已經分析過,我們知道當子類實現的tryAcquireShared()
方法的返回值小於0的時候,會導致當前線程進入自旋去嘗試獲取鎖。否則當返回值不小於0的時候,也就意味着當前線程已經獲取到了共享鎖。
tryAcquireShared(arg)
protected int tryAcquireShared(int acquires) {
return (getState() == 0) ? 1 : -1;
}
方法簡單到令人髮指,當state爲0的時候就返回1,否則就返回-1。所以只有當有count個線程調用了countDown()
方法後,纔會獲取到共享鎖繼續執行。AQS的設計就是這麼好,我們只需要寫很少的代碼就能實現一個鎖。