Java執行緒通訊
螣蛇乘霧,終為土灰。
多個執行緒協同工作完成某個任務時就會涉及到執行緒間通訊問題。如何使各個執行緒之間同時執行,順序執行、交叉執行等。
一、執行緒同時執行
建立兩個執行緒a和b,兩個執行緒內呼叫同一個列印 1-3 三個數字的方法。
1 package tjt; 2 3 import java.time.LocalDate; 4 5 public class Test { 6 7 /** 8 * 建立兩個執行緒a和b,兩個執行緒內呼叫同一個列印 1-3 三個數字的方法。 9 */ 10 private static void situationOne() { 11 Thread a = new Thread(new Runnable() { 12 @Override 13 public void run() { 14 doSomething("a"); 15 } 16 }); 17 Thread b = new Thread(new Runnable() { 18 @Override 19 public void run() { 20 doSomething("b"); 21 } 22 }); 23 a.start(); 24 b.start(); 25 } 26 27 /** 28 * 依次列印 1, 2, 3 三個數字 29 * 30 * @param threadName 31 */ 32 private static void doSomething(String threadName) { 33 int i = 0; 34 while (i++ < 3) { 35 try { 36 Thread.sleep(200); 37 } catch (InterruptedException e) { 38 e.printStackTrace(); 39 } 40 System.out.println(LocalDate.now() + " Thread " + threadName + " is doing, printing: " + i); 41 } 42 } 43 44 public static void main(String[] args) { 45 situationOne(); 46 } 47 }
多次執行發現a和b是同時列印的,無執行順序可言。
二、執行緒順序執行
建立兩個執行緒a和b,要求b 在 a 全部列印完後再開始列印。使用 thread.join() 方法,在子執行緒呼叫了join()方法後面的程式碼,只有等到子執行緒結束了才能執行,即必須a執行完畢後才輪到b。
1 package tjt; 2 3 import java.time.LocalDate; 4 5 public class Test { 6 7 /** 8 * 建立兩個執行緒a和b,要求b 在 a 全部列印完後再開始列印,使用 thread.join() 方法。 9 * 保證執行緒a執行完畢後才輪到b 10 */ 11 private static void situationOne() { 12 Thread a = new Thread(new Runnable() { 13 @Override 14 public void run() { 15 doSomething("a"); 16 } 17 }); 18 Thread b = new Thread(new Runnable() { 19 @Override 20 public void run() { 21 try { 22 System.out.println("執行緒 b 正在通過thread.join()等待執行緒 a 執行完畢後再潤"); 23 // thread.join() 在子執行緒呼叫了join()方法後面的程式碼,只有等到子執行緒結束了才能執行,即必須a執行完畢後才輪到b 24 a.join(); 25 } catch (InterruptedException e) { 26 e.printStackTrace(); 27 } 28 doSomething("b"); 29 } 30 }); 31 a.start(); 32 b.start(); 33 } 34 35 /** 36 * 依次列印 1, 2, 3 三個數字 37 * 38 * @param threadName 39 */ 40 private static void doSomething(String threadName) { 41 int i = 0; 42 while (i++ < 3) { 43 try { 44 Thread.sleep(200); 45 } catch (InterruptedException e) { 46 e.printStackTrace(); 47 } 48 System.out.println(LocalDate.now() + " Thread " + threadName + " is doing, printing: " + i); 49 } 50 } 51 52 public static void main(String[] args) { 53 situationOne(); 54 } 55 }
無論執行多少次,都是執行緒a先執行完畢再到執行緒b。
三、執行緒順序交叉執行
建立兩個執行緒a和b,要求 a 在列印完 1 後,再讓 b 列印 1、2、 3,接著再回到 a 繼續列印 2、3。如此順序交叉執行僅靠 Thread.join() 是無法滿足需求的,需要更細粒度的鎖來控制執行順序,以及object.wait() 和 object.notify() 兩個方法來實現。
1 package tjt; 2 3 import java.time.LocalDate; 4 5 public class TestAgain { 6 7 private static void situationTwo() { 8 // a 和 b 的共享物件鎖 lock 9 Object lock = new Object(); 10 Thread a = new Thread(new Runnable() { 11 @Override 12 public void run() { 13 // 同步鎖 lock 14 synchronized (lock) { 15 // a 獲得鎖後執行 16 doSomething("a", 1); 17 try { 18 // 呼叫 lock.wait() 方法,交出鎖的控制權,進入 wait 狀態,等待notify喚醒 19 lock.wait(); 20 } catch (InterruptedException e) { 21 e.printStackTrace(); 22 } 23 doSomething("a", 2); 24 doSomething("a", 3); 25 } 26 } 27 }); 28 Thread b = new Thread(new Runnable() { 29 @Override 30 public void run() { 31 // 同步鎖 lock 32 synchronized (lock) { 33 // a 獲得鎖後執行 34 doSomething("b", 1); 35 doSomething("b", 2); 36 doSomething("b", 3); 37 // 呼叫 lock.notify() 方法,喚醒正在 wait 的執行緒 a 38 lock.notify(); 39 } 40 } 41 }); 42 a.start(); 43 b.start(); 44 } 45 46 /** 47 * 列印 48 * 49 * @param threadName 50 * @param num 51 */ 52 private static void doSomething(String threadName, int num) { 53 System.out.println(LocalDate.now() + " Thread " + threadName + " is doing, printing: " + num); 54 } 55 56 public static void main(String[] args) { 57 situationTwo(); 58 } 59 }
無論執行多少次,都是執行緒a先執行列印1,然後執行緒b執行列印1、2、3,最後執行緒a執行列印2、3。
四、CountDownLatch
CountDownLatch 計數器適用於一個執行緒去等待多個執行緒的情況。例如A B C 三個執行緒同時執行,各自獨立執行完後通知執行緒 D 執行,就可以利用 CountdownLatch 來實現這類通訊方式。
對比之前的join方法,thread.join()可以讓一個執行緒等另一個執行緒執行完畢後再繼續執行,其可以在 D 執行緒裡依次 join A B C,但這樣 A B C 必須依次執行,無法實現ABC三者能同步執行。
1 package tjt; 2 3 import java.time.LocalDate; 4 import java.util.concurrent.CountDownLatch; 5 6 public class TestCountDownLatch { 7 8 /** 9 * countDownLatch 適用於一個執行緒去等待多個執行緒的情況 10 * 四個執行緒A、B、C、D, 11 * 其中 D 要等到 A B C 全執行完畢後才執行,且 A B C 是同步執行的,即ABC無順序執行 12 */ 13 private static void situationThree() { 14 // 初始計數值設定為3,即總共四個執行緒A、B、C、D 15 CountDownLatch latch = new CountDownLatch(3); 16 new Thread(new Runnable() { 17 @Override 18 public void run() { 19 System.out.println(LocalDate.now() + "執行緒 D 等待執行緒A B C 執行完畢後才可執行"); 20 try { 21 // await() 檢查計數器值是否為 0,若不為 0 則保持等待狀態 22 latch.await(); 23 // 其他執行緒 的 countDown() 方法把計數值變成 0 時,等待執行緒裡的 countDownLatch.await() 立即退出,繼續執行下面的程式碼 24 System.out.println(LocalDate.now() + "執行緒A B C 執行完畢,輪到執行緒D 執行了,當前latch:" + latch.getCount()); 25 } catch (InterruptedException e) { 26 e.printStackTrace(); 27 } 28 } 29 }).start(); 30 31 // 迴圈執行執行緒 A B C 32 for (char threadName = 'A'; threadName <= 'C'; threadName++) { 33 String name = String.valueOf(threadName); 34 new Thread(new Runnable() { 35 @Override 36 public void run() { 37 System.out.println("執行緒 " + name + " is running"); 38 // countDown(),將倒計數器減 1, 計數器被減至 0 時立即觸發D 的 await() 39 try { 40 Thread.sleep(200); 41 } catch (InterruptedException e) { 42 e.printStackTrace(); 43 } 44 System.out.println("執行緒 " + name + " 執行完畢計數器"); 45 latch.countDown(); 46 } 47 }).start(); 48 } 49 } 50 51 public static void main(String[] args) { 52 situationThree(); 53 } 54 55 }
五、CyclicBarrier
實現執行緒間互相等待,可以利用 CyclicBarrier 柵欄。CountDownLatch 可以用來倒計數,但當計數完畢,只有一個執行緒的 await() 會得到響應,無法讓多個執行緒同時觸發。如要求執行緒 A B C 各自開始準備,直到三者都準備完畢再同時執行其就無法滿足需求,而用CyclicBarrier則完全OK。
螣蛇乘霧
終為土灰