最近遇到多線程編程里面一個常見的問題:“如何讓主線程在全部子線程執(zhí)行完畢后再繼續(xù)執(zhí)行?”熙尉。經(jīng)過一番查找和實踐后就整理了幾種常見的實現(xiàn)方式
方法一:主線程sleep
主線程等待子線程執(zhí)行完最簡單的方式當(dāng)然是在主線程中Sleep一段時間,這種方式最簡單盆色,我們先看下實現(xiàn)
private static class MyThread extends Thread {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(String.format("%s %s was finished", DateUtils.format(new Date(), "hh:mm:ss:SSS"), getName()));
}
}
public static void main(String[] args) {
System.out.println(String.format("%s I was main and I'm started", DateUtils.format(new Date(), "hh:mm:ss:SSS")));
for (int i = 0; i < 10; i++) {
MyThread myThread = new MyThread();
myThread.start();
}
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(String.format("%s I was main and I'm finished", DateUtils.format(new Date(), "hh:mm:ss:SSS")));
}
但這種方式有個非常大的弊端:無法準(zhǔn)確地預(yù)估全部子線程執(zhí)行完畢的時間煤禽。時間太久,主線程就需要空等秧饮;時間太短,子線程又可能沒有全部執(zhí)行完畢泽篮。
方法二:子線程join
那么另外一種方式就是使用線程的Join()方法來實現(xiàn)主線程的等候盗尸,我們還是先來看下實現(xiàn)代碼
//此處省略MyThread代碼,同上
public static void main(String[] args) {
System.out.println(String.format("%s I was main and I'm started", DateUtils.format(new Date(), "hh:mm:ss:SSS")));
List<Thread> threads = new ArrayList<>();
for (int i = 0; i < 10; i++) {
MyThread myThread = new MyThread();
myThread.start();
threads.add(myThread);
}
for (Thread thread : threads) {
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(String.format("%s I was main and I'm finished", DateUtils.format(new Date(), "hh:mm:ss:SSS")));
}
可以看到我們在全部子線程開始執(zhí)行后帽撑,又在主線程中執(zhí)行全部子線程的join方法泼各,那么主線程會等待全部子線程執(zhí)行完畢后繼續(xù)往下執(zhí)行。這里放上Java 7 Concurrency Cookbook對join方法的定義亏拉,個人認(rèn)為比JDK中定義更準(zhǔn)確扣蜻。
join() method suspends the execution of the calling thread until the object called finishes its execution.
我們看下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;
}
}
}
可以看到逆巍,如果join的參數(shù)為0,那么主線程會一直判斷自己是否存活莽使,如果主線程存活锐极,則調(diào)用主線程的wait()方法,那么我們繼續(xù)看下wait方法的定義
/**
* Causes the current thread to wait until either another thread invokes the
* {@link java.lang.Object#notify()} method or the
* {@link java.lang.Object#notifyAll()} method for this object, or a
* specified amount of time has elapsed.
*/
public final native void wait(long timeout) throws InterruptedException;
大概意思是說wait(0)會使主線程進(jìn)入睡眠狀態(tài)芳肌,直到調(diào)用join方法的線程(簡稱t線程灵再,下同)執(zhí)行完畢后調(diào)用notify()或notifyAll()將其喚醒。注意這個地方有幾點需要特別說明:
代碼中沒有顯示調(diào)用notify或notifyAll的地方亿笤,這個喚醒操作其實是由于Java虛擬機在線程執(zhí)行完畢后所做的
主線程需要獲得t線程的對象鎖(wait 意味著拿到該對象的鎖)翎迁,然后進(jìn)入睡眠狀態(tài)
由于每個子線程都執(zhí)行了join方法,所以主線程需要等待全部子線程執(zhí)行完畢后才能被喚醒
方法三:使用CountDownLatch
另外我們還可以使用java.util.concurrent包里的CountDownLatch净薛,初始設(shè)置和子線程個數(shù)相同的計數(shù)器汪榔,子線程執(zhí)行完畢后計數(shù)器減1,直到全部子線程執(zhí)行完畢肃拜。注意countDownLatch不可能重新初始化或者修改CountDownLatch對象內(nèi)部計數(shù)器的值揍异,一個線程調(diào)用countdown方法happen-before另外一個線程調(diào)用await方法
private static CountDownLatch latch = new CountDownLatch(10);
public static void main(String[] args) {
System.out.println(String.format("%s I was main and I'm started", DateUtils.format(new Date(), "hh:mm:ss:SSS")));
for (int i = 0; i < 10; i++) {
MyThread myThread = new MyThread();
myThread.start();
}
try {
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(String.format("%s I was main and I'm finished", DateUtils.format(new Date(), "hh:mm:ss:SSS")));
}
關(guān)于CountDownLatch的具體實現(xiàn)這里不再詳細(xì)展開,如有興趣可以戳這里爆班,我們目前只需要直到await會使主線程阻塞直到計數(shù)器清零即可
方法四:使用CycleBarrier
另外還可以使用CycleBarrier實現(xiàn)主線程等待。CyclicBarrier 的字面意思是可循環(huán)使用(Cyclic)的屏障(Barrier)辱姨。它要做的事情是柿菩,讓一組線程到達(dá)一個屏障(也可以叫同步點)時被阻塞,直到最后一個線程到達(dá)屏障時雨涛,屏障才會開門枢舶,所有被屏障攔截的線程才會繼續(xù)干活。CyclicBarrier默認(rèn)的構(gòu)造方法是CyclicBarrier(int parties)替久,其參數(shù)表示屏障攔截的線程數(shù)量凉泄,每個線程調(diào)用await方法告訴CyclicBarrier我已經(jīng)到達(dá)了屏障,然后當(dāng)前線程被阻塞蚯根。
我們看下實現(xiàn)代碼
private static CyclicBarrier cyclicBarrier = new CyclicBarrier(10);
private static class MyThread extends Thread {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(String.format("%s %s was finished", DateUtils.format(new Date(), "hh:mm:ss:SSS"), getName()));
try {
cyclicBarrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) throws BrokenBarrierException {
System.out.println(String.format("%s I was main and I'm started", DateUtils.format(new Date(), "hh:mm:ss:SSS")));
for (int i = 0; i < 10; i++) {
MyThread myThread = new MyThread();
myThread.start();
}
try {
cyclicBarrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(String.format("%s I was main and I'm finished", DateUtils.format(new Date(), "hh:mm:ss:SSS")));
}
看完方法三和方法四后众,有人就要問了:“CountDownLatch和CyclicBarrier都能夠?qū)崿F(xiàn)線程之間的等待,這兩種方式有什么區(qū)別”颅拦。別急蒂誉,下面就來講下他們的區(qū)別
CountDownLatch一般用于某個線程A等待若干個其他線程執(zhí)行完任務(wù)之后,它才執(zhí)行距帅,而CyclicBarrier一般用于一組線程互相等待至某個狀態(tài)右锨,然后這一組線程再同時執(zhí)行;
CountDownLatch是不能夠重用的碌秸,而CyclicBarrier是可以重用的(reset)绍移。
執(zhí)行結(jié)果
由于四種方式的執(zhí)行結(jié)果大同小異悄窃,我們這里就放出兩種同步和異步的執(zhí)行結(jié)果