一道百度面試題的多種解法
題目
java程式,主程序需要等待多個子程序結束之後再執行後續的程式碼,有哪些方案可以實現?
這個需求其實我們在工作中經常會用到,比如使用者下單一個產品,後臺會做一系列的處理,為了提高效率,每個處理都可以用一個執行緒來執行,所有處理完成了之後才會返回給使用者下單成功,歡迎大家批評指正。
解法
1.join方法
使用Thread的join()等待所有的子執行緒執行完畢,主執行緒在執行,thread.join()把指定的執行緒加入到當前執行緒,可以將兩個交替執行的執行緒合併為順序執行的執行緒。比如線上程B中呼叫了執行緒A的join()方法,直到執行緒A執行完畢後,才會繼續執行執行緒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("子執行緒被執行"); } }); vector.add(childThread); childThread.start(); } for(Thread thread : vector) { thread.join(); } System.out.println("主執行緒被執行"); }
執行結果
子執行緒被執行 子執行緒被執行 子執行緒被執行 子執行緒被執行 子執行緒被執行 主執行緒被執行
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使用完畢後,它不能再次被使用。
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("子執行緒被執行"); latch.countDown(); } }); childThread.start(); } latch.await();//阻塞當前執行緒直到latch中的值 System.out.println("主執行緒被執行"); } }
執行結果:
子執行緒被執行 子執行緒被執行 子執行緒被執行 子執行緒被執行 子執行緒被執行 主執行緒被執行
3.同步屏障CyclicBarrier
這裡必須注意,CylicBarrier是控制一組執行緒的同步,初始化的引數:5的含義是包括主執行緒在內有5個執行緒,所以只能有四個子執行緒,這與CountDownLatch是不一樣的。
countDownLatch和cyclicBarrier有什麼區別呢,他們的區別:countDownLatch只能使用一次,而CyclicBarrier方法可以使用reset()方法重置,所以CyclicBarrier方法可以能處理更為複雜的業務場景。
我曾經在網上看到一個關於countDownLatch和cyclicBarrier的形象比喻,就是在百米賽跑的比賽中若使用 countDownLatch的話衝過終點線一個人就給評委傳送一個人的成績,10個人比賽傳送10次,如果用CyclicBarrier,則只在最後一個人衝過終點線的時候傳送所有人的資料,僅僅傳送一次,這就是區別。
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("子執行緒被執行"); 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();//阻塞當前執行緒直到latch中的值 System.out.println("主執行緒被執行"); } }
執行結果:
子執行緒被執行 子執行緒被執行 子執行緒被執行 子執行緒被執行 子執行緒被執行
4.使用yield方法(注意此種方法經過親自試驗證明並不可靠!)
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("子執行緒被執行"); } }); childThread.start(); } while (Thread.activeCount() > 2) {//保證前面的執行緒都執行完 Thread.yield(); } System.out.println("主執行緒被執行"); } }
執行結果:
子執行緒被執行 子執行緒被執行 子執行緒被執行 子執行緒被執行 子執行緒被執行 子執行緒被執行
為何yield方法會出現這樣的問題?
使當前執行緒從執行狀態(執行狀態)變為可執行態(就緒狀態)。cpu會從眾多的可執行態裡選擇,也就是說,當前也就是剛剛的那個執行緒還是有可能會被再次執行到的,並不是說一定會執行其他執行緒而該執行緒在下一次中不會執行到了。
Java執行緒中有一個Thread.yield( )方法,很多人翻譯成執行緒讓步。顧名思義,就是說當一個執行緒使用了這個方法之後,它就會把自己CPU執行的時間讓掉,讓自己或者其它的執行緒執行。
打個比方:現在有很多人在排隊上廁所,好不容易輪到這個人上廁所了,突然這個人說:“我要和大家來個競賽,看誰先搶到廁所!”,然後所有的人在同一起跑線衝向廁所,有可能是別人搶到了,也有可能他自己有搶到了。我們還知道執行緒有個優先順序的問題,那麼手裡有優先權的這些人就一定能搶到廁所的位置嗎? 不一定的,他們只是概率上大些,也有可能沒特權的搶到了。
yield的本質是把當前執行緒重新置入搶CPU時間的”佇列”(佇列只是說所有執行緒都在一個起跑線上.並非真正意義上的佇列)。
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.執行 Callable 方式,需要 FutureTask 實現類的支援,用於接收運算結果。 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(); //這裡獲取三個sum值只是為了同步,並沒有實際意義 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的作用,在所有的執行緒沒有執行完成之後這裡是不會執行的 System.out.println("主執行緒被執行"); } } 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("子執行緒被執行"); return sum; } }
6.使用callable+future
Callable+Future最終也是以Callable+FutureTask的形式實現的。
在這種方式中呼叫了: 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); //獲取執行緒執行結果,用來同步 Integer result1 = future1.get(); Integer result2 = future2.get(); System.out.println("主執行緒執行"); executor.shutdown(); } } class Task implements Callable<Integer>{ @Override public Integer call() throws Exception { int sum = 0; //do something; System.out.println("子執行緒被執行"); return sum; } }
執行結果:*
子執行緒被執行 子執行緒被執行 主執行緒執行
補充:
1)CountDownLatch和CyclicBarrier都能夠實現執行緒之間的等待,只不過它們側重點不同:
CountDownLatch一般用於某個執行緒A等待若干個其他執行緒執行完任務之後,它才執行;
而CyclicBarrier一般用於一組執行緒互相等待至某個狀態,然後這一組執行緒再同時執行;
另外,CountDownLatch是不能夠重用的,而CyclicBarrier是可以重用的。
2)Semaphore其實和鎖有點類似,它一般用於控制對某組資源的訪問許可權。
CountDownLatch類實際上是使用計數器的方式去控制的,不難想象當我們初始化CountDownLatch的時候傳入了一個int變數這個時候在類的內部初始化一個int的變數,每當我們呼叫countDownt()方法的時候就使得這個變數的值減1,而對於await()方法則去判斷這個int的變數的值是否為0,是則表示所有的操作都已經完成,否則繼續等待。
實際上如果瞭解AQS的話應該很容易想到可以使用AQS的共享式獲取同步狀態的方式來完成這個功能。而CountDownLatch實際上也就是這麼做的。