題目
java程序四啰,主進(jìn)程需要等待多個(gè)子進(jìn)程結(jié)束之后再執(zhí)行后續(xù)的代碼译隘,有哪些方案可以實(shí)現(xiàn)?
這個(gè)需求其實(shí)我們?cè)诠ぷ髦薪?jīng)常會(huì)用到梆造,比如用戶下單一個(gè)產(chǎn)品缴守,后臺(tái)會(huì)做一系列的處理,為了提高效率镇辉,每個(gè)處理都可以用一個(gè)線程來執(zhí)行屡穗,所有處理完成了之后才會(huì)返回給用戶下單成功,歡迎大家批評(píng)指正忽肛。
解法
1.join方法
使用Thread的join()等待所有的子線程執(zhí)行完畢村砂,主線程在執(zhí)行,thread.join()把指定的線程加入到當(dāng)前線程屹逛,可以將兩個(gè)交替執(zhí)行的線程合并為順序執(zhí)行的線程础废。比如在線程B中調(diào)用了線程A的join()方法,直到線程A執(zhí)行完畢后罕模,才會(huì)繼續(xù)執(zhí)行線程B评腺。
import java.util.Vector;
public class Test {
public static void main(String[] args) throws InterruptedException {
Vector<Thread> vector = new Vector<>();
for(int i=0;i<5;i++) {
Thread childThread= new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("子線程被執(zhí)行");
}
});
vector.add(childThread);
childThread.start();
}
for(Thread thread : vector) {
thread.join();
}
System.out.println("主線程被執(zhí)行");
}
執(zhí)行結(jié)果
子線程被執(zhí)行
子線程被執(zhí)行
子線程被執(zhí)行
子線程被執(zhí)行
子線程被執(zhí)行
主線程被執(zhí)行
2.等待多線程完成的CountDownLatch
CountDownLatch的概念
CountDownLatch是一個(gè)同步工具類,用來協(xié)調(diào)多個(gè)線程之間的同步淑掌,或者說起到線程之間的通信(而不是用作互斥的作用)蒿讥。
CountDownLatch能夠使一個(gè)線程在等待另外一些線程完成各自工作之后,再繼續(xù)執(zhí)行抛腕。使用一個(gè)計(jì)數(shù)器進(jìn)行實(shí)現(xiàn)芋绸。計(jì)數(shù)器初始值為線程的數(shù)量。當(dāng)每一個(gè)線程完成自己任務(wù)后担敌,計(jì)數(shù)器的值就會(huì)減一摔敛。當(dāng)計(jì)數(shù)器的值為0時(shí),表示所有的線程都已經(jīng)完成了任務(wù)柄错,然后在CountDownLatch上等待的線程就可以恢復(fù)執(zhí)行任務(wù)舷夺。
CountDownLatch的用法
CountDownLatch典型用法1:某一線程在開始運(yùn)行前等待n個(gè)線程執(zhí)行完畢苦酱。將CountDownLatch的計(jì)數(shù)器初始化為n new CountDownLatch(n) 售貌,每當(dāng)一個(gè)任務(wù)線程執(zhí)行完畢,就將計(jì)數(shù)器減1 countdownlatch.countDown()疫萤,當(dāng)計(jì)數(shù)器的值變?yōu)?時(shí)颂跨,在CountDownLatch上 await() 的線程就會(huì)被喚醒。一個(gè)典型應(yīng)用場(chǎng)景就是啟動(dòng)一個(gè)服務(wù)時(shí)扯饶,主線程需要等待多個(gè)組件加載完畢恒削,之后再繼續(xù)執(zhí)行池颈。
CountDownLatch典型用法2:實(shí)現(xiàn)多個(gè)線程開始執(zhí)行任務(wù)的最大并行性。注意是并行性钓丰,不是并發(fā)躯砰,強(qiáng)調(diào)的是多個(gè)線程在某一時(shí)刻同時(shí)開始執(zhí)行。類似于賽跑携丁,將多個(gè)線程放到起點(diǎn)琢歇,等待發(fā)令槍響,然后同時(shí)開跑梦鉴。做法是初始化一個(gè)共享的CountDownLatch(1)李茫,將其計(jì)數(shù)器初始化為1,多個(gè)線程在開始執(zhí)行任務(wù)前首先 coundownlatch.await()肥橙,當(dāng)主線程調(diào)用 countDown() 時(shí)魄宏,計(jì)數(shù)器變?yōu)?,多個(gè)線程同時(shí)被喚醒存筏。
CountDownLatch的不足
CountDownLatch是一次性的宠互,計(jì)數(shù)器的值只能在構(gòu)造方法中初始化一次,之后沒有任何機(jī)制再次對(duì)其設(shè)置值方篮,當(dāng)CountDownLatch使用完畢后名秀,它不能再次被使用。
import java.util.Vector;
import java.util.concurrent.CountDownLatch;
public class Test2 {
public static void main(String[] args) throws InterruptedException {
final CountDownLatch latch = new CountDownLatch(5);
for(int i=0;i<5;i++) {
Thread childThread= new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("子線程被執(zhí)行");
latch.countDown();
}
});
childThread.start();
}
latch.await();//阻塞當(dāng)前線程直到latch中的值
System.out.println("主線程被執(zhí)行");
}
}
執(zhí)行結(jié)果:
子線程被執(zhí)行
子線程被執(zhí)行
子線程被執(zhí)行
子線程被執(zhí)行
子線程被執(zhí)行
主線程被執(zhí)行
3.同步屏障CyclicBarrier
這里必須注意藕溅,CylicBarrier是控制一組線程的同步匕得,初始化的參數(shù):5的含義是包括主線程在內(nèi)有5個(gè)線程,所以只能有四個(gè)子線程巾表,這與CountDownLatch是不一樣的汁掠。
countDownLatch和cyclicBarrier有什么區(qū)別呢,他們的區(qū)別:countDownLatch只能使用一次集币,而CyclicBarrier方法可以使用reset()方法重置考阱,所以CyclicBarrier方法可以能處理更為復(fù)雜的業(yè)務(wù)場(chǎng)景。
我曾經(jīng)在網(wǎng)上看到一個(gè)關(guān)于countDownLatch和cyclicBarrier的形象比喻鞠苟,就是在百米賽跑的比賽中若使用 countDownLatch的話沖過終點(diǎn)線一個(gè)人就給評(píng)委發(fā)送一個(gè)人的成績(jī)乞榨,10個(gè)人比賽發(fā)送10次,如果用CyclicBarrier当娱,則只在最后一個(gè)人沖過終點(diǎn)線的時(shí)候發(fā)送所有人的數(shù)據(jù)吃既,僅僅發(fā)送一次,這就是區(qū)別跨细。
package interview;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
public class Test3 {
public static void main(String[] args) throws InterruptedException, BrokenBarrierException {
final CyclicBarrier barrier = new CyclicBarrier(5);
for(int i=0;i<4;i++) {
Thread childThread= new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("子線程被執(zhí)行");
try {
barrier.await();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (BrokenBarrierException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
});
childThread.start();
}
barrier.await();//阻塞當(dāng)前線程直到latch中的值
System.out.println("主線程被執(zhí)行");
}
}
執(zhí)行結(jié)果:
子線程被執(zhí)行
子線程被執(zhí)行
子線程被執(zhí)行
子線程被執(zhí)行
子線程被執(zhí)行
4.使用yield方法(注意此種方法經(jīng)過親自試驗(yàn)證明并不可靠p幸小)
public class Test4 {
public static void main(String[] args) throws InterruptedException {
for(int i=0;i<5;i++) {
Thread childThread= new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("子線程被執(zhí)行");
}
});
childThread.start();
}
while (Thread.activeCount() > 2) { //保證前面的線程都執(zhí)行完
Thread.yield();
}
System.out.println("主線程被執(zhí)行");
}
}
執(zhí)行結(jié)果:
子線程被執(zhí)行
子線程被執(zhí)行
子線程被執(zhí)行
子線程被執(zhí)行
子線程被執(zhí)行
子線程被執(zhí)行
為何yield方法會(huì)出現(xiàn)這樣的問題?
使當(dāng)前線程從執(zhí)行狀態(tài)(運(yùn)行狀態(tài))變?yōu)榭蓤?zhí)行態(tài)(就緒狀態(tài))冀惭。cpu會(huì)從眾多的可執(zhí)行態(tài)里選擇震叙,也就是說掀鹅,當(dāng)前也就是剛剛的那個(gè)線程還是有可能會(huì)被再次執(zhí)行到的,并不是說一定會(huì)執(zhí)行其他線程而該線程在下一次中不會(huì)執(zhí)行到了媒楼。
Java線程中有一個(gè)Thread.yield( )方法乐尊,很多人翻譯成線程讓步。顧名思義划址,就是說當(dāng)一個(gè)線程使用了這個(gè)方法之后科吭,它就會(huì)把自己CPU執(zhí)行的時(shí)間讓掉,讓自己或者其它的線程運(yùn)行猴鲫。
打個(gè)比方:現(xiàn)在有很多人在排隊(duì)上廁所对人,好不容易輪到這個(gè)人上廁所了,突然這個(gè)人說:“我要和大家來個(gè)競(jìng)賽拂共,看誰(shuí)先搶到廁所牺弄!”,然后所有的人在同一起跑線沖向廁所宜狐,有可能是別人搶到了势告,也有可能他自己有搶到了。我們還知道線程有個(gè)優(yōu)先級(jí)的問題抚恒,那么手里有優(yōu)先權(quán)的這些人就一定能搶到廁所的位置嗎? 不一定的咱台,他們只是概率上大些,也有可能沒特權(quán)的搶到了俭驮。
yield的本質(zhì)是把當(dāng)前線程重新置入搶CPU時(shí)間的”隊(duì)列”(隊(duì)列只是說所有線程都在一個(gè)起跑線上.并非真正意義上的隊(duì)列)回溺。
5.FutureTast可用于閉鎖,類似于CountDownLatch的作用
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class Test5 {
public static void main(String[] args) {
MyThread td = new MyThread();
//1.執(zhí)行 Callable 方式混萝,需要 FutureTask 實(shí)現(xiàn)類的支持遗遵,用于接收運(yùn)算結(jié)果。
FutureTask<Integer> result1 = new FutureTask<>(td);
new Thread(result1).start();
FutureTask<Integer> result2 = new FutureTask<>(td);
new Thread(result2).start();
FutureTask<Integer> result3 = new FutureTask<>(td);
new Thread(result3).start();
Integer sum;
try {
sum = result1.get();
sum = result2.get();
sum = result3.get();
//這里獲取三個(gè)sum值只是為了同步逸嘀,并沒有實(shí)際意義
System.out.println(sum);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (ExecutionException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} //FutureTask 可用于 閉鎖 類似于CountDownLatch的作用车要,在所有的線程沒有執(zhí)行完成之后這里是不會(huì)執(zhí)行的
System.out.println("主線程被執(zhí)行");
}
}
class MyThread implements Callable<Integer> {
@Override
public Integer call() throws Exception {
int sum = 0;
Thread.sleep(1000);
for (int i = 0; i <= 10; i++) {
sum += i;
}
System.out.println("子線程被執(zhí)行");
return sum;
}
}
6.使用callable+future
Callable+Future最終也是以Callable+FutureTask的形式實(shí)現(xiàn)的。
在這種方式中調(diào)用了: Future future = executor.submit(task);
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class Test6 {
public static void main(String[] args) throws InterruptedException, ExecutionException {
ExecutorService executor = Executors.newCachedThreadPool();
Task task = new Task();
Future<Integer> future1 = executor.submit(task);
Future<Integer> future2 = executor.submit(task);
//獲取線程執(zhí)行結(jié)果崭倘,用來同步
Integer result1 = future1.get();
Integer result2 = future2.get();
System.out.println("主線程執(zhí)行");
executor.shutdown();
}
}
class Task implements Callable<Integer>{
@Override public Integer call() throws Exception {
int sum = 0;
//do something;
System.out.println("子線程被執(zhí)行");
return sum;
}
}
執(zhí)行結(jié)果:*
子線程被執(zhí)行
子線程被執(zhí)行
主線程執(zhí)行
補(bǔ)充:
1)CountDownLatch和CyclicBarrier都能夠?qū)崿F(xiàn)線程之間的等待翼岁,只不過它們側(cè)重點(diǎn)不同:
CountDownLatch一般用于某個(gè)線程A等待若干個(gè)其他線程執(zhí)行完任務(wù)之后,它才執(zhí)行司光;
而CyclicBarrier一般用于一組線程互相等待至某個(gè)狀態(tài)琅坡,然后這一組線程再同時(shí)執(zhí)行;
另外飘庄,CountDownLatch是不能夠重用的脑蠕,而CyclicBarrier是可以重用的购撼。
2)Semaphore其實(shí)和鎖有點(diǎn)類似跪削,它一般用于控制對(duì)某組資源的訪問權(quán)限谴仙。
CountDownLatch類實(shí)際上是使用計(jì)數(shù)器的方式去控制的,不難想象當(dāng)我們初始化CountDownLatch的時(shí)候傳入了一個(gè)int變量這個(gè)時(shí)候在類的內(nèi)部初始化一個(gè)int的變量碾盐,每當(dāng)我們調(diào)用countDownt()方法的時(shí)候就使得這個(gè)變量的值減1晃跺,而對(duì)于await()方法則去判斷這個(gè)int的變量的值是否為0,是則表示所有的操作都已經(jīng)完成毫玖,否則繼續(xù)等待掀虎。
實(shí)際上如果了解AQS的話應(yīng)該很容易想到可以使用AQS的共享式獲取同步狀態(tài)的方式來完成這個(gè)功能。而CountDownLatch實(shí)際上也就是這么做的付枫。