一道百度java面試題的多種解法


下面是我在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 }
View Code

執行結果:

子線程被執行
子線程被執行
子線程被執行
子線程被執行
主線程被執行

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM