下面是我在2018年10月11日二面百度的時候的一個問題:
java程序,主進程需要等待多個子進程結束之后再執行后續的代碼,有哪些方案可以實現?
這個需求其實我們在工作中經常會用到,比如用戶下單一個產品,后台會做一系列的處理,為了提高效率,每個處理都可以用一個線程來執行,所有處理完成了之后才會返回給用戶下單成功,歡迎大家批評指正:
1.join方法
使用Thread的join()等待所有的子線程執行完畢,主線程在執行,thread.join()把指定的線程加入到當前線程,可以將兩個交替執行的線程合並為順序執行的線程。比如在線程B中調用了線程A的join()方法,直到線程A執行完畢后,才會繼續執行線程B。
示例:
1 import java.util.Vector; 2
3 public class Test { 4 public static void main(String[] args) throws InterruptedException { 5 Vector<Thread> vector = new Vector<>(); 6 for(int i=0;i<5;i++) { 7 Thread childThread= new Thread(new Runnable() { 8
9 @Override 10 public void run() { 11 // TODO Auto-generated method stub
12 try { 13 Thread.sleep(1000); 14 } catch (InterruptedException e) { 15 // TODO Auto-generated catch block
16 e.printStackTrace(); 17 } 18 System.out.println("子線程被執行"); 19 } 20
21 }); 22 vector.add(childThread); 23 childThread.start(); 24 } 25 for(Thread thread : vector) { 26 thread.join(); 27 } 28 System.out.println("主線程被執行"); 29 }
執行結果:
子線程被執行
子線程被執行
子線程被執行
子線程被執行
子線程被執行
主線程被執行
2.等待多線程完成的CountDownLatch
CountDownLatch的概念
CountDownLatch是一個同步工具類,用來協調多個線程之間的同步,或者說起到線程之間的通信(而不是用作互斥的作用)。
CountDownLatch能夠使一個線程在等待另外一些線程完成各自工作之后,再繼續執行。使用一個計數器進行實現。計數器初始值為線程的數量。當每一個線程完成自己任務后,計數器的值就會減一。當計數器的值為0時,表示所有的線程都已經完成了任務,然后在CountDownLatch上等待的線程就可以恢復執行任務。
CountDownLatch的用法
CountDownLatch典型用法1:某一線程在開始運行前等待n個線程執行完畢。將CountDownLatch的計數器初始化為n new CountDownLatch(n) ,每當一個任務線程執行完畢,就將計數器減1 countdownlatch.countDown(),當計數器的值變為0時,在CountDownLatch上 await() 的線程就會被喚醒。一個典型應用場景就是啟動一個服務時,主線程需要等待多個組件加載完畢,之后再繼續執行。
CountDownLatch典型用法2:實現多個線程開始執行任務的最大並行性。注意是並行性,不是並發,強調的是多個線程在某一時刻同時開始執行。類似於賽跑,將多個線程放到起點,等待發令槍響,然后同時開跑。做法是初始化一個共享的CountDownLatch(1),將其計數器初始化為1,多個線程在開始執行任務前首先 coundownlatch.await(),當主線程調用 countDown() 時,計數器變為0,多個線程同時被喚醒。
CountDownLatch的不足
CountDownLatch是一次性的,計數器的值只能在構造方法中初始化一次,之后沒有任何機制再次對其設置值,當CountDownLatch使用完畢后,它不能再次被使用。
1 import java.util.Vector; 2 import java.util.concurrent.CountDownLatch; 3
4 public class Test2 { 5 public static void main(String[] args) throws InterruptedException { 6 final CountDownLatch latch = new CountDownLatch(5); 7 for(int i=0;i<5;i++) { 8 Thread childThread= new Thread(new Runnable() { 9
10 @Override 11 public void run() { 12 // TODO Auto-generated method stub
13 try { 14 Thread.sleep(1000); 15 } catch (InterruptedException e) { 16 // TODO Auto-generated catch block
17 e.printStackTrace(); 18 } 19 System.out.println("子線程被執行"); 20 latch.countDown(); 21 } 22
23 }); 24
25 childThread.start(); 26
27 } 28 latch.await();//阻塞當前線程直到latch中的值
29 System.out.println("主線程被執行"); 30 } 31
32 }
執行結果:
子線程被執行
子線程被執行
子線程被執行
子線程被執行
子線程被執行
主線程被執行
3.同步屏障CyclicBarrier
這里必須注意,CylicBarrier是控制一組線程的同步,初始化的參數:5的含義是包括主線程在內有5個線程,所以只能有四個子線程,這與CountDownLatch是不一樣的。
countDownLatch和cyclicBarrier有什么區別呢,他們的區別:countDownLatch只能使用一次,而CyclicBarrier方法可以使用reset()方法重置,所以CyclicBarrier方法可以能處理更為復雜的業務場景。
我曾經在網上看到一個關於countDownLatch和cyclicBarrier的形象比喻,就是在百米賽跑的比賽中若使用 countDownLatch的話沖過終點線一個人就給評委發送一個人的成績,10個人比賽發送10次,如果用CyclicBarrier,則只在最后一個人沖過終點線的時候發送所有人的數據,僅僅發送一次,這就是區別。

1 package interview; 2
3 import java.util.concurrent.BrokenBarrierException; 4 import java.util.concurrent.CyclicBarrier; 5
6 public class Test3 { 7 public static void main(String[] args) throws InterruptedException, BrokenBarrierException { 8 final CyclicBarrier barrier = new CyclicBarrier(5); 9 for(int i=0;i<4;i++) { 10 Thread childThread= new Thread(new Runnable() { 11
12 @Override 13 public void run() { 14 // TODO Auto-generated method stub
15 try { 16 Thread.sleep(1000); 17 } catch (InterruptedException e) { 18 // TODO Auto-generated catch block
19 e.printStackTrace(); 20 } 21 System.out.println("子線程被執行"); 22 try { 23 barrier.await(); 24 } catch (InterruptedException e) { 25 // TODO Auto-generated catch block
26 e.printStackTrace(); 27 } catch (BrokenBarrierException e) { 28 // TODO Auto-generated catch block
29 e.printStackTrace(); 30 } 31 } 32
33 }); 34
35 childThread.start(); 36
37 } 38 barrier.await();//阻塞當前線程直到latch中的值
39 System.out.println("主線程被執行"); 40 } 41 }
執行結果:
子線程被執行
子線程被執行
子線程被執行
子線程被執行
主線程被執行
4.使用yield方法(注意此種方法經過親自試驗證明並不可靠!)
1 public class Test4 { 2 public static void main(String[] args) throws InterruptedException { 3 for(int i=0;i<5;i++) { 4 Thread childThread= new Thread(new Runnable() { 5
6 @Override 7 public void run() { 8 // TODO Auto-generated method stub
9 try { 10 Thread.sleep(1000); 11 } catch (InterruptedException e) { 12 // TODO Auto-generated catch block
13 e.printStackTrace(); 14 } 15 System.out.println("子線程被執行"); 16
17 } 18
19 }); 20
21 childThread.start(); 22
23 } 24 while (Thread.activeCount() > 2) { //保證前面的線程都執行完
25 Thread.yield(); 26 } 27 System.out.println("主線程被執行"); 28 } 29 }
執行結果:
1 子線程被執行 2 子線程被執行 3 子線程被執行 4 子線程被執行 5 主線程被執行 6 子線程被執行
為何yield方法會出現這樣的問題?
使當前線程從執行狀態(運行狀態)變為可執行態(就緒狀態)。cpu會從眾多的可執行態里選擇,也就是說,當前也就是剛剛的那個線程還是有可能會被再次執行到的,並不是說一定會執行其他線程而該線程在下一次中不會執行到了。
Java線程中有一個Thread.yield( )方法,很多人翻譯成線程讓步。顧名思義,就是說當一個線程使用了這個方法之后,它就會把自己CPU執行的時間讓掉,讓自己或者其它的線程運行。
打個比方:現在有很多人在排隊上廁所,好不容易輪到這個人上廁所了,突然這個人說:“我要和大家來個競賽,看誰先搶到廁所!”,然后所有的人在同一起跑線沖向廁所,有可能是別人搶到了,也有可能他自己有搶到了。我們還知道線程有個優先級的問題,那么手里有優先權的這些人就一定能搶到廁所的位置嗎? 不一定的,他們只是概率上大些,也有可能沒特權的搶到了。
yield的本質是把當前線程重新置入搶CPU時間的”隊列”(隊列只是說所有線程都在一個起跑線上.並非真正意義上的隊列)。
5.FutureTast可用於閉鎖,類似於CountDownLatch的作用
1 import java.util.concurrent.Callable; 2 import java.util.concurrent.ExecutionException; 3 import java.util.concurrent.FutureTask; 4 5 public class Test5 { 6 public static void main(String[] args) { 7 MyThread td = new MyThread(); 8 9 //1.執行 Callable 方式,需要 FutureTask 實現類的支持,用於接收運算結果。 10 FutureTask<Integer> result1 = new FutureTask<>(td); 11 new Thread(result1).start(); 12 FutureTask<Integer> result2 = new FutureTask<>(td); 13 new Thread(result2).start(); 14 FutureTask<Integer> result3 = new FutureTask<>(td); 15 new Thread(result3).start(); 16 17 Integer sum; 18 try { 19 sum = result1.get(); 20 sum = result2.get(); 21 sum = result3.get(); 22 //這里獲取三個sum值只是為了同步,並沒有實際意義 23 System.out.println(sum); 24 } catch (InterruptedException e) { 25 // TODO Auto-generated catch block 26 e.printStackTrace(); 27 } catch (ExecutionException e) { 28 // TODO Auto-generated catch block 29 e.printStackTrace(); 30 } //FutureTask 可用於 閉鎖 類似於CountDownLatch的作用,在所有的線程沒有執行完成之后這里是不會執行的 31 32 System.out.println("主線程被執行"); 33 34 } 35 36 } 37 38 class MyThread implements Callable<Integer> { 39 40 @Override 41 public Integer call() throws Exception { 42 int sum = 0; 43 Thread.sleep(1000); 44 for (int i = 0; i <= 10; i++) { 45 sum += i; 46 } 47 System.out.println("子線程被執行"); 48 return sum; 49 } 50 }
6.使用callable+future
Callable+Future最終也是以Callable+FutureTask的形式實現的。
在這種方式中調用了: Future future = executor.submit(task);
1 import java.util.concurrent.Callable; 2 import java.util.concurrent.ExecutionException; 3 import java.util.concurrent.ExecutorService; 4 import java.util.concurrent.Executors; 5 import java.util.concurrent.Future; 6 7 public class Test6 { 8 public static void main(String[] args) throws InterruptedException, ExecutionException { 9 ExecutorService executor = Executors.newCachedThreadPool(); 10 Task task = new Task(); 11 Future<Integer> future1 = executor.submit(task); 12 Future<Integer> future2 = executor.submit(task); 13 //獲取線程執行結果,用來同步 14 Integer result1 = future1.get(); 15 Integer result2 = future2.get(); 16 17 System.out.println("主線程執行"); 18 executor.shutdown(); 19 } 20 } 21 class Task implements Callable<Integer>{ 22 @Override public Integer call() throws Exception { 23 int sum = 0; 24 //do something; 25 System.out.println("子線程被執行"); 26 return sum; 27 } 28 }
執行結果:
子線程被執行
子線程被執行
主線程執行
補充:
1)CountDownLatch和CyclicBarrier都能夠實現線程之間的等待,只不過它們側重點不同:
CountDownLatch一般用於某個線程A等待若干個其他線程執行完任務之后,它才執行;
而CyclicBarrier一般用於一組線程互相等待至某個狀態,然后這一組線程再同時執行;
另外,CountDownLatch是不能夠重用的,而CyclicBarrier是可以重用的。
2)Semaphore其實和鎖有點類似,它一般用於控制對某組資源的訪問權限。
CountDownLatch類實際上是使用計數器的方式去控制的,不難想象當我們初始化CountDownLatch的時候傳入了一個int變量這個時候在類的內部初始化一個int的變量,每當我們調用countDownt()方法的時候就使得這個變量的值減1,而對於await()方法則去判斷這個int的變量的值是否為0,是則表示所有的操作都已經完成,否則繼續等待。
實際上如果了解AQS的話應該很容易想到可以使用AQS的共享式獲取同步狀態的方式來完成這個功能。而CountDownLatch實際上也就是這么做的。
參考文獻:
https://blog.csdn.net/u011277123/article/details/54015755/
https://blog.csdn.net/joenqc/article/details/76794356
https://blog.csdn.net/weixin_38553453/article/details/72921797
https://blog.csdn.net/LightOfMiracle/article/details/73456832
https://www.cnblogs.com/baizhanshi/p/6425209.html