獲得多個子執行緒的結果,面試和工作中你會遇到的多執行緒問題
昨天專案組裡的一名畢業生詢問我,如何知道非同步執行緒的返回值,這讓我不覺想起來了兩年前我參加招商系一個公司的面試,在技術二面時,面試官出過一道這樣的程式設計題。
題目大概含義是:我有一個需求是為了得到一個求和結果,但是這個結果,需要兩個耗時大概1s 左右計算功能的結果相加得到的,麻煩用執行緒幫我實現,方案越多越好,然後默默的遞過來紙和筆~~
首先我們分析一下,這個題目肯定是需要多個執行緒執行的,我們要抓住這個題目的關鍵點 : 主執行緒必須要等2個子執行緒執行完,拿到子執行緒的結果進行相加,得到最終結果。
其實,實現這個題目的方式有很多種,以我現在的觀點來看,面試官應該主要考察如下三點:
1. 多執行緒相關的基本知識點理解是否透徹,thread沒有返回值如何處理。
2. 思維是否活躍,知識面是否廣,能想出多少種方案
3. 寫程式碼是否規範
當年too young,我只寫出了方案一和方案二的部分程式碼(由於平時敲程式碼自動提示的比較多,所以很多單詞敲不出來)。
不知道面試官當時是怎樣的心境,反正最終是拿到offer了,不過因為個人原因,去了另一家公司。
接下來,我將兩個數相加,相個數相乘模仿兩個耗時的計算功能,用三種方案來解決這個問題。(我面試的時候寫的程式碼可沒有下面這麼詳細)
方案一: 使用thread.join()實現
java中的join方法可以控制執行緒的執行順序,這個方案主要是考察執行緒的join方法原理,以及thread的實現方式。
join() method suspends the execution of the calling thread until the object called finishes its execution.
大概的意思是:如果在主執行緒mian()中呼叫子執行緒的join()方法,就會阻塞主執行緒,直到子執行緒執行完,在喚起主執行緒繼續執行。
至於為什麼會阻塞主執行緒,有興趣的同學可以繼續找資料學習,這裡就不多擴充套件了。
1 package day01; 2 3 public class JoinTest { 4public static void main(String[] args) { 5CalculateThread addTread = new CalculateThread(2, 3, "add"); 6CalculateThread mutlTread = new CalculateThread(2, 3, "mutl"); 7addTread.start(); // 子執行緒處理兩個數相加 8mutlTread.start(); // 子執行緒處理兩個數相乘 9try { 10addTread.join(); // 暫停主執行緒,讓addTread先執行完 11mutlTread.join(); // 暫停主執行緒,讓mutlTread子執行緒先執行完 12} catch (InterruptedException e) { 13e.printStackTrace(); 14} 15 16int threadResult = addTread.getResult() + mutlTread.getResult(); 17System.out.println("主執行緒獲得兩個子執行緒結果的和:" + threadResult); 18} 19 20 } 21 22 class CalculateThread extends Thread { 23private int param1; 24private int param2; 25private String type; 26private int result; //用來儲存執行緒執行結果 27 28public CalculateThread(int param1, int param2, String type) { 29this.param1 = param1; 30this.param2 = param2; 31this.type = type; 32} 33 34// 為了獲取每個子執行緒計算結果 35public int getResult() { 36return result; 37} 38 39// 兩個數相加 40public void add() { 41System.out.println(this.getName() + ":子執行緒處理兩個引數相加開始"); 42try { 43Thread.sleep(2000L); // 模擬加法子執行緒需要執行2s 44 45result = param1 + param2; 46 47} catch (InterruptedException e) { 48e.printStackTrace(); 49} 50System.out.println(this.getName() + ":子執行緒處理兩個引數相加結束"); 51} 52 53// 兩個數相乘 54public void mult() { 55System.out.println(this.getName() + ":子執行緒處理兩個引數相乘開始"); 56try { 57Thread.sleep(1000L); // 模擬乘法子執行緒需要執行1s 58 59result = param1 * param2; 60 61} catch (InterruptedException e) { 62e.printStackTrace(); 63} 64System.out.println(this.getName() + ":子執行緒處理兩個引數相乘結束"); 65} 66 67@Override 68public void run() { 69// TODO Auto-generated method stub 70switch (type) { 71case "add": // switch的string型別,jdk 1.7才開始支援 72add(); 73break; 74case "mutl": 75mult(); 76break; 77default: 78break; 79} 80} 81 82
方案一注意:
1. join()一定要在start()方法之後呼叫。所以如果多個子執行緒執行,要先迴圈執行完子執行緒的start()方法,再迴圈執行join()方法,這樣才能變成並行執行。
如果執行一個子執行緒的start()方法後,就直接執行這個子執行緒的join()方法,由於主執行緒阻塞主了,所以需要等這個執行緒執行完,後面的執行緒才能執行,就變成序列的了。
2. 每個子執行緒計算的返回值,我們目前是用子執行緒裡的變數儲存實現的,我們也可以用主執行緒的引用型別當作共享變數實現(這個要考慮併發下,執行緒安全問題)。
方案二:使用Future和Callable實現
Future是可以儲存返回值的,這也是很多人知道的方案,Future封裝了多個方法,可以很好的獲取執行緒執行狀態,以及異常處理,這裡我們就不擴充套件了,有興趣的同學可以自己再去學習。
1 package day01; 2 3 import java.util.concurrent.Callable; 4 import java.util.concurrent.ExecutionException; 5 import java.util.concurrent.ExecutorService; 6 import java.util.concurrent.Executors; 7 import java.util.concurrent.Future; 8 9 public class FutureTest { 10public static void main(String[] args) { 11try { 12CalculateCallable addCalable = new CalculateCallable(2, 3, "add"); 13CalculateCallable mutlCalable = new CalculateCallable(2, 3, "mutl"); 14// new 兩個執行緒,固定執行緒 15ExecutorService execute = Executors.newFixedThreadPool(2); 16// 返回 Futrue物件後,可以使用 Futrue.get() 方法獲取返回值 17Future<Integer> addFuture = execute.submit(addCalable); 18Future<Integer> mutlFuture = execute.submit(mutlCalable); 19 20int futurResult = addFuture.get() + mutlFuture.get(); 21System.out.println("主執行緒獲得兩個子執行緒結果的和:" + futurResult); 22 23} catch (ExecutionException e) { 24e.printStackTrace(); 25} catch (InterruptedException e1) { 26e1.printStackTrace(); 27} 28} 29 } 30 31 class CalculateCallable implements Callable<Integer> { 32private int param1; 33private int param2; 34private String type; 35 36public CalculateCallable(int param1, int param2, String type) { 37this.param1 = param1; 38this.param2 = param2; 39this.type = type; 40} 41 42// 兩個數相加 43public int add() { 44System.out.println("加法子執行緒處理兩個引數相加開始"); 45try { 46Thread.sleep(2000L); // 模擬加法子執行緒需要執行2s 47} catch (InterruptedException e) { 48e.printStackTrace(); 49} 50System.out.println("加法子執行緒處理兩個引數相加結束"); 51 52return param1 + param2; 53} 54 55// 兩個數相乘 56public int mult() { 57System.out.println("乘法子執行緒處理兩個引數相乘開始"); 58try { 59Thread.sleep(1000L); // 模擬乘法子執行緒需要執行1s 60} catch (InterruptedException e) { 61e.printStackTrace(); 62} 63System.out.println("乘法子執行緒處理兩個引數相乘結束"); 64return param1 * param2; 65 66} 67 68@Override 69public Integer call() throws Exception { 70// TODO Auto-generated method stub 71switch (type) { 72case "add": // switch的string型別,jdk 1.7才開始支援 73return add(); 74case "mutl": 75return mult(); 76default: 77return null; 78} 79} 80 81 }
方案二注意:
1. Future介面呼叫get()方法取得處理的結果值時是阻塞性的,如果呼叫Future物件的get()方法時,任務尚未執行完成,則呼叫get()方法時一直阻塞到此任務完成為止。
如果這樣,則前面先執行的任務一旦耗時很多,後面的任務呼叫get()方法就呈阻塞狀態,也就是排隊進行等待。主執行緒並不能保證首先獲得結果的是最先完成任務執行緒的返回值,大大影響執行效率。那麼使用多執行緒就沒什麼意義了。
幸運的是JDK併發包也提供了CompletionService介面可以解決這個問題,它的take()方法哪個執行緒先完成就先獲取誰的Futrue物件,有興趣的可以去仔細瞭解下相關知識點。
方案三:使用CountDownLatch實現
這個方案,當時面試的時候沒想到,但是是一個很好很強大的併發類。
CountDownLatch 存在於java.util.concurrent包下。CountDownLatch這個類能夠使一個執行緒等待其他執行緒完成各自的工作後再執行。例如,應用程式的主執行緒希望負責啟動框架服務的執行緒在已經啟動所有的框架服務之後再執行。
CountDownLatch是通過一個計數器來實現的,計數器的初始值為執行緒的數量。每當一個執行緒完成了自己的任務後,計數器的值就會減1。當計數器值到達0時,它表示所有的執行緒已經完成了任務,然後在閉鎖上等待的執行緒就可以恢復執行任務。
1 package day01; 2 3 import java.util.concurrent.CountDownLatch; 4 import java.util.concurrent.TimeUnit; 5 6 public class CountDownLatchTest { 7public static void main(String[] args) { 8// 初始值為2,因為我們目前就2個子執行緒 9CountDownLatch countDownLatch = new CountDownLatch(2); 10// 每個執行緒中傳入countDownLatch 11CountDownThread addTread = new CountDownThread(2,3,"add",countDownLatch); 12CountDownThread mutlTread = new CountDownThread(2,3,"mutl",countDownLatch); 13addTread.start(); 14mutlTread.start(); 15try{ 16// 設定超時時間為3秒,3s如果執行緒沒有執行完,返回false 17boolean timeoutFlag = countDownLatch.await(3,TimeUnit.SECONDS); 18if(timeoutFlag){ 19int threadResult = addTread.getResult() + mutlTread.getResult(); 20System.out.println("主執行緒獲得兩個子執行緒結果的和:" + threadResult); 21}else{ 22int threadResult = addTread.getResult() + mutlTread.getResult(); 23System.out.println("主執行緒等待子執行緒執行超時:" + threadResult); 24} 25 26}catch (InterruptedException e){ 27e.printStackTrace(); 28} 29} 30 } 31 32 33 class CountDownThread extends Thread{ 34private int param1; 35private int param2; 36private String type; 37private int result; 38private CountDownLatch countDownLatch; 39 40public CountDownThread(int param1,int param2,String type,CountDownLatch countDownLatch){ 41this.param1 = param1; 42this.param2 = param2; 43this.type = type; 44this.countDownLatch = countDownLatch; 45} 46 47// 為了獲取每個子執行緒計算結果 48public int getResult() { 49return result; 50} 51 52// 兩個數相加 53public void add(){ 54System.out.println(this.getName()+":子執行緒處理兩個引數相加開始"); 55try{ 56Thread.sleep(2000L); //模擬加法子執行緒需要執行2s 57 58result = param1 + param2; 59 60System.out.println(this.getName()+":子執行緒處理兩個引數相加結束"); 61 62}catch(InterruptedException e){ 63e.printStackTrace(); 64}finally{ 65// 計數器減1 66countDownLatch.countDown(); 67} 68} 69 70// 兩個數相乘 71public void mult(){ 72System.out.println(this.getName()+":子執行緒處理兩個引數相乘開始"); 73try{ 74Thread.sleep(1000L); //模擬乘法子執行緒需要執行1s 75 76result = param1 * param2; 77 78System.out.println(this.getName()+":子執行緒處理兩個引數相乘結束"); 79 80}catch(InterruptedException e){ 81e.printStackTrace(); 82}finally{ 83// 計數器減1 84countDownLatch.countDown(); 85} 86} 87 88@Override 89public void run() { 90// TODO Auto-generated method stub 91switch (type) { 92case "add": //switch的string型別,jdk 1.7才開始支援 93add(); 94break; 95case "mutl": 96mult(); 97break; 98default: 99break; 100} 101} 102 }
如果我們將超時時間改成1s ,boolean timeoutFlag = countDownLatch.await(1,TimeUnit.SECONDS); 由於計算相加時睡眠了2s,相乘時睡眠了1s
所以,相加的計算是直接超時的,timeoutFlag會返回false,最終計算結果是2 * 3 = 6。
方案三注意:
1. CountDownLatch的缺點是CountDownLatch是一次性的,計數器的值只能在構造方法中初始化一次,之後沒有任何機制再次對其設定值,當CountDownLatch使用完畢後,它不能再次被使用。
2. countDownLatch.countDown() 最好在finally塊中執行,防止子執行緒沒有執行完,就自減1了,導致主執行緒沒有等到所有子執行緒執行完,便執行了。
其實我們在工作中遇到的業務場景往往比較複雜,對併發,異常的處理都比較嚴格,這裡只是給大家提供一個方向,以後遇到類似的功能需求,我們不會像無頭蒼蠅一樣,至少有了解決問題的方向。