在併發操作中,當需要當前線程c等待另一線程a結束後在運行的話,我們首先想到的是join方法,在c線程運行中調用a.join(),該方法會使當前線程阻塞於a,直到線程a運行結束,JVM調用a.notifyAll()方法喚醒z。
在Java1.5之後,併發包提供的CountDownLatch也可以實現join功能,並且更爲強大。
一、使用方法
下面放出一個簡單的Demo:
public static void main(String[]args){
CountDownLatch c = new CountDownLatch(2);
Thread a = new Thread(new Runnable(){
@Override
public void run() {
try {
Thread.currentThread().sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("a");
c.countDown();
}
});
Thread b = new Thread(new Runnable(){
@Override
public void run() {
try {
Thread.currentThread().sleep(2000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("b");
c.countDown();
}
});
a.start();
b.start();
try {
c.await();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("main");
}
/*
a
b
main
*/
CountDownLatch的構造函數接收一個int類型的參數作爲計數器,如果你想等待N個點完成,這裏就傳入N。每當調用CountDownLatch的countDown方法,N就會減1。CountDownLatch的await方法會阻塞當前線程,知道N變爲0。由於countDown方法可以用在任何地方,所以既可以是N個線程, 也可以是1個線程N個步驟。在使用多線程時,只需要把這個CountDownLatch的引用傳入線程即可。此外,CountDownLatch也允許多個線程調用await方法,同時阻塞多個線程。
還有一點要注意的是,CountDownLatch的計數器無法被重置。
二、實現原理
同讀寫鎖一樣,CountDownLatch本質上也是一個共享鎖。它允許一個或多個線程等待其他線程。它的實驗原理也是同讀寫鎖一樣,通過隊列同步器(AbstractQueuedSynchronizer AQS)實現的:
構造方法:
public CountDownLatch(int count) {
if (count < 0) throw new IllegalArgumentException("count < 0");
this.sync = new Sync(count);
}
構造器傳入的計數count被用來構造一個Sync(AQS)對象。Sync的構造函數如下:
Sync(int count) {
setState(count); //設置同步狀態
}
這裏,同步器的state就是一個鎖狀態,當state大於0時,鎖被佔用,阻塞的線程就不能被喚醒。
阻塞方法:
public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
這裏調用了同步器的共享式獲取同步狀態,如果當前線程未獲取同步,則會進入同步隊列等待。並且此方法響應中斷。同步器中該方法源碼如下:
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException(); //響應中斷
if (tryAcquireShared(arg) < 0) //嘗試獲取共享鎖
doAcquireSharedInterruptibly(arg); //嘗試失敗 線程進入阻塞
}
該方法爲模板方法,關鍵在於重寫的tryAcquireShared方法,如下:
protected int tryAcquireShared(int acquires) {
return (getState() == 0) ? 1 : -1;
}
嘗試獲取共享鎖,當鎖計數器==0,即鎖是可以獲取的,則返回1,獲取鎖成功,直接退出acquireSharedInterruptibly方法。否則返回-1,進入doAcquireSharedInterruptibly方法阻塞當前線程。doAcquireSharedInterruptibly方法源碼如下:
private void doAcquireSharedInterruptibly(int arg)
throws InterruptedException {
final Node node = addWaiter(Node.SHARED); //創建當前線程的Node節點,且標記爲共享鎖,將鎖加入CLH隊列末尾
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor(); //獲取前繼節點
if (p == head) {
int r = tryAcquireShared(arg); //嘗試獲取鎖
if (r >= 0) { //如果成功
setHeadAndPropagate(node, r); //設置爲頭結點
p.next = null; // help GC
failed = false;
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) && //如果不是頭結點,則繼續等待
parkAndCheckInterrupt())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
countDown方法
public void countDown() {
sync.releaseShared(1);
}
實際上調用隊列同步器的釋放鎖方法: public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
首先會嘗試直接釋放共享鎖,成功則直接返回,否則調用doReleaseShared();
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; //狀態-1
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}
}
三、總結
CountDownLatch是通過共享鎖實現的。在創建CountDownLatch中時,會傳遞一個int類型參數count,該參數是隊列同步器中的state狀態值,表示共享鎖最多能被count線程同時獲取。當某線程調用該CountDownLatch的await()方法時,該線程會等待共享鎖可用時,才能獲取共享鎖。而共享鎖的可用條件就是state等於0。只有每次調用CountDownLatch的countDown方法,狀態值纔會減1。