一、概述
CountDownLatch是一個同步工具類,它允許一個或多個線程等待其他線程執行完操作之后再繼續執行。通常用于控制多個線程的執行順序。
二、基本原理
我們可以把CountDownLatch看成是一個計數器,其內部維護著一個count計數,計數器的初始化值為需要控制的線程的數量,比如需要控制幾個線程順序執行我們就初始化傳入幾,之后每當其中一個線程完成了自己的任務后,就調用
countDown()
來使計數器減1;而在調用者線程中需要調用await()
方法使得當前調用者線程一直處于阻塞狀態,直至當計數器到達0時,就表明其他所有的線程都已經完成了任務,然后處于阻塞狀態的調用者線程才可以繼續往下執行。
三、應用場景
假如有這樣一個需求,我們當前有一個任務,然后我們把這個任務進行分解成多個步驟完成,這個時候我們可以考慮使用多線程,每個線程完成一個步驟,等到所有的步驟都完成之后,程序提示任務完成。
對于這個需求,通常我們要實現主線程等待所有線程完成任務之后才可繼續操作,最為簡單的做法是直接使用join()
方法,代碼如下:
package com.feizi.java.concurrency.tool;
import java.util.concurrent.TimeUnit;
/**
* Created by feizi on 2018/5/30.
*/
public class JoinTest {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
try {
System.out.println("step one has finished...");
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
try {
System.out.println("step two has finished...");
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("all thread has finished...");
}
}
控制臺輸出結果:
step one has finished...
step two has finished...
all thread has finished...
Process finished with exit code 0
join()方法主要用于讓當前執行線程等到join線程執行結束才可繼續執行。其實現原理就是不停地去檢查join線程是否處于存活狀態while (isAlive())
,如果join線程存活則讓當前線程永遠wait,我們可以看下源碼,wait(0)
表示永遠等待下去。
join()的源碼:
public final void join() throws InterruptedException {
join(0);
}
繼續跟進去:
public final synchronized void join(long millis)
throws InterruptedException {
long base = System.currentTimeMillis();
long now = 0;
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (millis == 0) {
while (isAlive()) {
wait(0);
}
} else {
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break;
}
wait(delay);
now = System.currentTimeMillis() - base;
}
}
}
我們可以看到,重點是這一句,如果存活,就調用wait(0)
,使得永遠等待。
while (isAlive()) {
wait(0);
}
直到join線程中止后,線程的this.notifyAll()
方法才會被調用,調用notifyAll是在JVM里實現的,所以JDK里看不到。
而在JDK1.5之后的并發包中提供的CountDownLatch也可以實現join的這個功能,并且比join的功能更多。
四、CountDownLatch使用
4.1、例子1-CountDownLatchTest.java類:
package com.feizi.java.concurrency.tool;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
/**
* Created by feizi on 2018/5/30.
*/
public class CountDownLatchTest {
private static CountDownLatch latch = new CountDownLatch(2);
public static void main(String[] args) throws InterruptedException {
new Thread(new Runnable() {
@Override
public void run() {
//步驟一完成
System.out.println("step one has finished...");
try {
//模擬步驟一耗時操作
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
//步驟一完成,調用countDown()方法,計數器就減1
latch.countDown();
//步驟二完成
System.out.println("step two has finished...");
try {
//模擬步驟二耗時操作
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
//步驟二完成,調用countDown()方法,計數器就減1
latch.countDown();
}
}).start();
//步驟一和步驟二完成之前會阻塞住
latch.await();
//直到所有的步驟都完成,主線程才繼續執行
System.out.println("all steps have finished...");
}
}
控制臺輸出結果:
step one has finished...
step two has finished...
all steps have finished...
Process finished with exit code 0
4.2、例子2-CountDownLatchTest2.java類:
package com.feizi.java.concurrency.tool;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
/**
* Created by feizi on 2018/5/30.
*/
public class CountDownLatchTest2 {
private static CountDownLatch latch = new CountDownLatch(2);
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
try {
System.out.println("step one has finished...");
TimeUnit.SECONDS.sleep(1);
//計數器減一
latch.countDown();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
try {
TimeUnit.SECONDS.sleep(1);
System.out.println("step two has finished...");
//計數器減一
latch.countDown();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t1.start();
t2.start();
//計數器清零之前,阻塞住當前線程
latch.await();
System.out.println("all steps have finished...");
}
}
控制臺輸出結果:
step one has finished...
step two has finished...
all steps have finished...
Process finished with exit code 0
4.3、例子3-CountDownLatchTest3.java類
package com.feizi.java.concurrency.tool;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
/**
* Created by feizi on 2018/5/30.
*/
public class CountDownLatchTest3 {
public static void main(String[] args) throws InterruptedException {
/*線程計數器*/
CountDownLatch latch = new CountDownLatch(2);
Thread t1 = new Thread(new StepOneThread(latch));
Thread t2 = new Thread(new StepTwoThread(latch));
t1.start();
t2.start();
//調用await()阻塞當前線程,直至計數器清零才可繼續執行
latch.await();
TimeUnit.SECONDS.sleep(2);
System.out.println("all steps has finished...");
}
}
/**
* 步驟一線程
*/
class StepOneThread implements Runnable{
private CountDownLatch latch;
public StepOneThread(CountDownLatch latch) {
this.latch = latch;
}
@Override
public void run() {
try {
System.out.println("step one has finished...");
//模擬步驟一耗時操作
TimeUnit.SECONDS.sleep(2);
//步驟一完成計數器減一
latch.countDown();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
/**
* 步驟二線程
*/
class StepTwoThread implements Runnable{
private CountDownLatch latch;
public StepTwoThread(CountDownLatch latch) {
this.latch = latch;
}
@Override
public void run() {
try {
//模擬步驟二耗時操作
TimeUnit.SECONDS.sleep(2);
System.out.println("step two has finished...");
//步驟二完成計數器減一
latch.countDown();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
控制臺輸出結果:
step one has finished...
step two has finished...
all steps has finished...
Process finished with exit code 0
通過上述簡單的例子,我們可以看到CountDownLatch的構造函數接收一個int類型的參數作為計數器count,如果你想等待n個任務完成后再執行,那么這里就直接傳入n即可。
當我們調用一次CountDownLatch的
countDown()
方法時,計數器count便會減1,而CountDownLatch的await()
方法則會一直阻塞住當前線程,直至計數器count變為0。
最佳實踐:上面所說的n個任務,可以是n個線程(上述例子2和例子3),也可以是1個線程里的n個執行步驟(上述例子1),需要注意的是在運用于多個線程時,我們只需要把這個CountDownLatch的引用傳遞到線程里即可。
五、其他方法
最后需要注意的是,如果有某個任務特別耗時,而我們又不可能讓調用者線程(比如主線程)一直等待下去,那么就可以指定等待的時間,比如這個方法await(long time, TimeUnit unit)
:此方法在等待指定的時間之后不會再阻塞在當前線程,另外join也有類似的方法。
六、注意
當我們初始化一個CountDownLatch時將其計數器初始化為0,則在調用await()
方法時不會阻塞當前線程。比如:
CountDownLatch latch = new CountDownLatch(0);