讓執行緒按順序執行 8 種方法

  • 2019 年 10 月 5 日
  • 筆記

一.前言

本文使用了8種方法實現在多執行緒中讓執行緒按順序運行的方法,涉及到多執行緒中許多常用的方法,不止為了知道如何讓執行緒按順序運行,更是讓讀者對多執行緒的使用有更深刻的了解。使用的方法如下:

[1] 使用執行緒的join方法

二.實現

我們下面需要完成這樣一個應用場景:

1.早上;2.測試人員、產品經理、開發人員陸續的來公司上班;3.產品經理規劃新需求;4.開發人員開發新需求功能;5.測試人員測試新功能。

規劃需求,開發需求新功能,測試新功能是一個有順序的,我們把thread1看做產品經理,thread2看做開發人員,thread3看做測試人員。

1.使用執行緒的join方法

join():是Theard的方法,作用是調用執行緒需等待該join()執行緒執行完成後,才能繼續用下運行。

應用場景:當一個執行緒必須等待另一個執行緒執行完畢才能執行時可以使用join方法。

package com.wwj.javabase.thread.order;    /**   * @author wwj   * 通過子程式join使執行緒按順序執行   */  public class ThreadJoinDemo {        public static void main(String[] args) {          final Thread thread1 = new Thread(new Runnable() {              @Override              public void run() {                  System.out.println("產品經理規劃新需求");              }          });            final Thread thread2 = new Thread(new Runnable() {              @Override              public void run() {                  try {                      thread1.join();                      System.out.println("開發人員開發新需求功能");                  } catch (InterruptedException e) {                      e.printStackTrace();                  }              }          });            Thread thread3 = new Thread(new Runnable() {              @Override              public void run() {                  try {                      thread2.join();                      System.out.println("測試人員測試新功能");                  } catch (InterruptedException e) {                      e.printStackTrace();                  }              }          });            System.out.println("早上:");          System.out.println("測試人員來上班了...");          thread3.start();          System.out.println("產品經理來上班了...");          thread1.start();          System.out.println("開發人員來上班了...");          thread2.start();      }  }

運行結果

早上:

2.使用主執行緒的join方法

這裡是在主執行緒中使用join()來實現對執行緒的阻塞。

package com.wwj.javabase.thread.order;    /**   * @author wwj   * 通過主程式join使執行緒按順序執行   */  public class ThreadMainJoinDemo {        public static void main(String[] args) throws Exception {            final Thread thread1 = new Thread(new Runnable() {              @Override              public void run() {                  System.out.println("產品經理正在規劃新需求...");              }          });            final Thread thread2 = new Thread(new Runnable() {              @Override              public void run() {                  System.out.println("開發人員開發新需求功能");              }          });            final Thread thread3 = new Thread(new Runnable() {              @Override              public void run() {                  System.out.println("測試人員測試新功能");              }          });            System.out.println("早上:");          System.out.println("產品經理來上班了");          System.out.println("測試人員來上班了");          System.out.println("開發人員來上班了");          thread1.start();          //在父進程調用子進程的join()方法後,父進程需要等待子進程運行完再繼續運行。          System.out.println("開發人員和測試人員休息會...");          thread1.join();          System.out.println("產品經理新需求規劃完成!");          thread2.start();          System.out.println("測試人員休息會...");          thread2.join();          thread3.start();      }  }

運行結果

產品經理來上班了

3.使用執行緒的wait方法

wait():是Object的方法,作用是讓當前執行緒進入等待狀態,同時,wait()也會讓當前執行緒釋放它所持有的鎖。「直到其他執行緒調用此對象的 notify() 方法或 notifyAll() 方法」,當前執行緒被喚醒(進入「就緒狀態」)

notify()和notifyAll():是Object的方法,作用則是喚醒當前對象上的等待執行緒;notify()是喚醒單個執行緒,而notifyAll()是喚醒所有的執行緒。

wait(long timeout):讓當前執行緒處於「等待(阻塞)狀態」,「直到其他執行緒調用此對象的notify()方法或 notifyAll() 方法,或者超過指定的時間量」,當前執行緒被喚醒(進入「就緒狀態」)。

應用場景:Java實現生產者消費者的方式。

package com.wwj.javabase.thread.order;    /**   * @author wwj   */  public class ThreadWaitDemo {        private static Object myLock1 = new Object();      private static Object myLock2 = new Object();        /**       * 為什麼要加這兩個標識狀態?       * 如果沒有狀態標識,當t1已經運行完了t2才運行,t2在等待t1喚醒導致t2永遠處於等待狀態       */      private static Boolean t1Run = false;      private static Boolean t2Run = false;      public static void main(String[] args) {            final Thread thread1 = new Thread(new Runnable() {              @Override              public void run() {                  synchronized (myLock1){                      System.out.println("產品經理規劃新需求...");                      t1Run = true;                      myLock1.notify();                  }              }          });            final Thread thread2 = new Thread(new Runnable() {              @Override              public void run() {                  synchronized (myLock1){                      try {                          if(!t1Run){                              System.out.println("開發人員先休息會...");                              myLock1.wait();                          }                          synchronized (myLock2){                              System.out.println("開發人員開發新需求功能");                              myLock2.notify();                          }                      } catch (InterruptedException e) {                          e.printStackTrace();                      }                  }              }          });            Thread thread3 = new Thread(new Runnable() {              @Override              public void run() {                  synchronized (myLock2){                      try {                          if(!t2Run){                              System.out.println("測試人員先休息會...");                              myLock2.wait();                          }                          System.out.println("測試人員測試新功能");                      } catch (InterruptedException e) {                          e.printStackTrace();                      }                  }              }          });            System.out.println("早上:");          System.out.println("測試人員來上班了...");          thread3.start();          System.out.println("產品經理來上班了...");          thread1.start();          System.out.println("開發人員來上班了...");          thread2.start();      }  }

運行結果:這裡輸出會有很多種順序,主要是因為執行緒進入的順序,造成鎖住執行緒的順序不一致。

早上:

4.使用執行緒的執行緒池方法

JAVA通過Executors提供了四種執行緒池

  • 單執行緒化執行緒池(newSingleThreadExecutor);
  • 可控最大並發數執行緒池(newFixedThreadPool);
  • 可回收快取執行緒池(newCachedThreadPool);
  • 支援定時與周期性任務的執行緒池(newScheduledThreadPool)。

單執行緒化執行緒池(newSingleThreadExecutor):優點,串列執行所有任務。

submit():提交任務。

shutdown():方法用來關閉執行緒池,拒絕新任務。

應用場景:串列執行所有任務。如果這個唯一的執行緒因為異常結束,那麼會有一個新的執行緒來替代它。此執行緒池保證所有任務的執行順序按照任務的提交順序執行。

package com.wwj.javabase.thread.order;    import java.util.concurrent.ExecutorService;  import java.util.concurrent.Executors;    /**   * @author wwj   * 通過SingleThreadExecutor讓執行緒按順序執行   */  public class ThreadPoolDemo {        static ExecutorService executorService = Executors.newSingleThreadExecutor();        public static void main(String[] args) throws Exception {            final Thread thread1 = new Thread(new Runnable() {              @Override              public void run() {                  System.out.println("產品經理規劃新需求");              }          });            final Thread thread2 = new Thread(new Runnable() {              @Override              public void run() {                  System.out.println("開發人員開發新需求功能");              }          });            Thread thread3 = new Thread(new Runnable() {              @Override              public void run() {                  System.out.println("測試人員測試新功能");              }          });            System.out.println("早上:");          System.out.println("產品經理來上班了");          System.out.println("測試人員來上班了");          System.out.println("開發人員來上班了");          System.out.println("領導吩咐:");          System.out.println("首先,產品經理規劃新需求...");          executorService.submit(thread1);          System.out.println("然後,開發人員開發新需求功能...");          executorService.submit(thread2);          System.out.println("最後,測試人員測試新功能...");          executorService.submit(thread3);          executorService.shutdown();      }  }

運行結果

早上:

5.使用執行緒的Condition(條件變數)方法

Condition(條件變數):通常與一個鎖關聯。需要在多個Contidion中共享一個鎖時,可以傳遞一個Lock/RLock實例給構造方法,否則它將自己生成一個RLock實例。

  • Condition中await()方法類似於Object類中的wait()方法。
  • Condition中await(long time,TimeUnit unit)方法類似於Object類中的wait(long time)方法。
  • Condition中signal()方法類似於Object類中的notify()方法。
  • Condition中signalAll()方法類似於Object類中的notifyAll()方法。

應用場景:Condition是一個多執行緒間協調通訊的工具類,使得某個,或者某些執行緒一起等待某個條件(Condition),只有當該條件具備( signal 或者 signalAll方法被調用)時 ,這些等待執行緒才會被喚醒,從而重新爭奪鎖。

package com.wwj.javabase.thread.order;    import java.util.concurrent.locks.Condition;  import java.util.concurrent.locks.Lock;  import java.util.concurrent.locks.ReentrantLock;    /**   * @author wwj   * 使用Condition(條件變數)實現執行緒按順序運行   */  public class ThreadConditionDemo {        private static Lock lock = new ReentrantLock();      private static Condition condition1 = lock.newCondition();      private static Condition condition2 = lock.newCondition();        /**       * 為什麼要加這兩個標識狀態?       * 如果沒有狀態標識,當t1已經運行完了t2才運行,t2在等待t1喚醒導致t2永遠處於等待狀態       */      private static Boolean t1Run = false;      private static Boolean t2Run = false;        public static void main(String[] args) {            final Thread thread1 = new Thread(new Runnable() {              @Override              public void run() {                  lock.lock();                  System.out.println("產品經理規劃新需求");                  t1Run = true;                  condition1.signal();                  lock.unlock();              }          });            final Thread thread2 = new Thread(new Runnable() {              @Override              public void run() {                  lock.lock();                  try {                      if(!t1Run){                          System.out.println("開發人員先休息會...");                          condition1.await();                      }                      System.out.println("開發人員開發新需求功能");                      t2Run = true;                      condition2.signal();                  } catch (InterruptedException e) {                      e.printStackTrace();                  }                  lock.unlock();              }          });            Thread thread3 = new Thread(new Runnable() {              @Override              public void run() {                  lock.lock();                  try {                      if(!t2Run){                          System.out.println("測試人員先休息會...");                          condition2.await();                      }                      System.out.println("測試人員測試新功能");                      lock.unlock();                  } catch (InterruptedException e) {                      e.printStackTrace();                  }              }          });            System.out.println("早上:");          System.out.println("測試人員來上班了...");          thread3.start();          System.out.println("產品經理來上班了...");          thread1.start();          System.out.println("開發人員來上班了...");          thread2.start();      }  }

運行結果:這裡輸出會有很多種順序,主要是因為執行緒進入的順序,造成鎖住執行緒的順序不一致

早上:

6.使用執行緒的CountDownLatch(倒計數)方法

CountDownLatch:位於java.util.concurrent包下,利用它可以實現類似計數器的功能。

應用場景:比如有一個任務C,它要等待其他任務A,B執行完畢之後才能執行,此時就可以利用CountDownLatch來實現這種功能了。

package com.wwj.javabase.thread.order;    import java.util.concurrent.CountDownLatch;    /**   * @author wwj   * 通過CountDownLatch(倒計數)使執行緒按順序執行   */  public class ThreadCountDownLatchDemo {        /**       * 用於判斷執行緒一是否執行,倒計時設置為1,執行後減1       */      private static CountDownLatch c1 = new CountDownLatch(1);        /**       * 用於判斷執行緒二是否執行,倒計時設置為1,執行後減1       */      private static CountDownLatch c2 = new CountDownLatch(1);        public static void main(String[] args) {          final Thread thread1 = new Thread(new Runnable() {              @Override              public void run() {                  System.out.println("產品經理規劃新需求");                  //對c1倒計時-1                  c1.countDown();              }          });            final Thread thread2 = new Thread(new Runnable() {              @Override              public void run() {                  try {                      //等待c1倒計時,計時為0則往下運行                      c1.await();                      System.out.println("開發人員開發新需求功能");                      //對c2倒計時-1                      c2.countDown();                  } catch (InterruptedException e) {                      e.printStackTrace();                  }              }          });            Thread thread3 = new Thread(new Runnable() {              @Override              public void run() {                  try {                      //等待c2倒計時,計時為0則往下運行                      c2.await();                      System.out.println("測試人員測試新功能");                  } catch (InterruptedException e) {                      e.printStackTrace();                  }              }          });            System.out.println("早上:");          System.out.println("測試人員來上班了...");          thread3.start();          System.out.println("產品經理來上班了...");          thread1.start();          System.out.println("開發人員來上班了...");          thread2.start();      }  }

運行結果

早上:

7.使用CyclicBarrier(迴環柵欄)實現執行緒按順序運行

CyclicBarrier(迴環柵欄):通過它可以實現讓一組執行緒等待至某個狀態之後再全部同時執行。叫做迴環是因為當所有等待執行緒都被釋放以後,CyclicBarrier可以被重用。我們暫且把這個狀態就叫做barrier,當調用await()方法之後,執行緒就處於barrier了。

應用場景:公司組織春遊,等待所有的員工到達集合地點才能出發,每個人到達後進入barrier狀態。都到達後,喚起大家一起出發去旅行。

package com.wwj.javabase.thread.order;    import java.util.concurrent.BrokenBarrierException;  import java.util.concurrent.CyclicBarrier;    /**   * @author wwj   * 使用CyclicBarrier(迴環柵欄)實現執行緒按順序運行   */  public class CyclicBarrierDemo {        static CyclicBarrier barrier1 = new CyclicBarrier(2);      static CyclicBarrier barrier2 = new CyclicBarrier(2);        public static void main(String[] args) {            final Thread thread1 = new Thread(new Runnable() {              @Override              public void run() {                  try {                      System.out.println("產品經理規劃新需求");                      //放開柵欄1                      barrier1.await();                  } catch (InterruptedException e) {                      e.printStackTrace();                  } catch (BrokenBarrierException e) {                      e.printStackTrace();                  }              }          });            final Thread thread2 = new Thread(new Runnable() {              @Override              public void run() {                  try {                      //放開柵欄1                      barrier1.await();                      System.out.println("開發人員開發新需求功能");                      //放開柵欄2                      barrier2.await();                  } catch (InterruptedException e) {                      e.printStackTrace();                  } catch (BrokenBarrierException e) {                      e.printStackTrace();                  }              }          });            final Thread thread3 = new Thread(new Runnable() {              @Override              public void run() {                  try {                      //放開柵欄2                      barrier2.await();                      System.out.println("測試人員測試新功能");                  } catch (InterruptedException e) {                      e.printStackTrace();                  } catch (BrokenBarrierException e) {                      e.printStackTrace();                  }              }          });            System.out.println("早上:");          System.out.println("測試人員來上班了...");          thread3.start();          System.out.println("產品經理來上班了...");          thread1.start();          System.out.println("開發人員來上班了...");          thread2.start();      }  }

運行結果

早上:

8.使用Sephmore(訊號量)實現執行緒按順序運行

Sephmore(訊號量):Semaphore是一個計數訊號量,從概念上將,Semaphore包含一組許可證,如果有需要的話,每個acquire()方法都會阻塞,直到獲取一個可用的許可證,每個release()方法都會釋放持有許可證的執行緒,並且歸還Semaphore一個可用的許可證。然而,實際上並沒有真實的許可證對象供執行緒使用,Semaphore只是對可用的數量進行管理維護。

acquire():當前執行緒嘗試去阻塞的獲取1個許可證,此過程是阻塞的,當前執行緒獲取了1個可用的許可證,則會停止等待,繼續執行。

release():當前執行緒釋放1個可用的許可證。

應用場景:Semaphore可以用來做流量分流,特別是對公共資源有限的場景,比如資料庫連接。假設有這個的需求,讀取幾萬個文件的數據到資料庫中,由於文件讀取是IO密集型任務,可以啟動幾十個執行緒並發讀取,但是資料庫連接數只有10個,這時就必須控制最多只有10個執行緒能夠拿到資料庫連接進行操作。這個時候,就可以使用Semaphore做流量控制。

package com.wwj.javabase.thread.order;    import java.util.concurrent.Semaphore;  /**   * @author wwj   * 使用Sephmore(訊號量)實現執行緒按順序運行   */  public class SemaphoreDemo {      private static Semaphore semaphore1 = new Semaphore(1);      private static Semaphore semaphore2 = new Semaphore(1);      public static void main(String[] args) {          final Thread thread1 = new Thread(new Runnable() {              @Override              public void run() {                  System.out.println("產品經理規劃新需求");                  semaphore1.release();              }          });            final Thread thread2 = new Thread(new Runnable() {              @Override              public void run() {                  try {                      semaphore1.acquire();                      System.out.println("開發人員開發新需求功能");                      semaphore2.release();                  } catch (InterruptedException e) {                      e.printStackTrace();                  }              }          });            Thread thread3 = new Thread(new Runnable() {              @Override              public void run() {                  try {                      semaphore2.acquire();                      thread2.join();                      semaphore2.release();                      System.out.println("測試人員測試新功能");                  } catch (InterruptedException e) {                      e.printStackTrace();                  }              }          });            System.out.println("早上:");          System.out.println("測試人員來上班了...");          thread3.start();          System.out.println("產品經理來上班了...");          thread1.start();          System.out.println("開發人員來上班了...");          thread2.start();      }  }

運行結果

早上: