一起聊聊3個執行緒依次列印1、2、3…的故事

  • 2020 年 3 月 10 日
  • 筆記

3個執行緒依次列印1、2、3…這個問題,常常被作為面試題,題目如下:

三個執行緒,一個執行緒負責列印1,4,7,……;第二個負責列印2,5,8,……,第三個負責列印3,6,9,……,要求在控制台中按順序輸出1,2,3,4,5,6……。

這個題目肯定是要啟動3個執行緒的,那怎麼讓這3個執行緒「協作」按順序列印1、2、3呢?從大的方面來講,這種「協作」可分為以下兩種:

  • 競爭型:每個執行緒都搶著去列印,如果發現不該自己列印,則準備下一輪搶。由於大家都是競爭的,因此需要用鎖機制來保護。
  • 協同型:當前執行緒執行緒列印之後通知下一個執行緒去列印,這種需要確認好第一個執行緒列印時機。由於是協同型的因此可以不用鎖機制來保護,但是需要一個通知機制。

競爭型列印

多個執行緒競爭型列印,優勢是程式碼簡單易懂,劣勢是執行緒爭搶是CPU調度進行的,可能該某個執行緒列印時結果該執行緒遲遲未被CPU調度,結果其他執行緒被CPU調度到但是由於不能執行列印操作而繼續爭搶,造成CPU性能浪費。示例程式碼如下:

public class DemoTask implements Runnable {        // 這裡將lock對象換成 Lock(ReentrantLock) 進行lock/unlock也是可以的      private static final Object lock = new Object();      private static final int MAX = 1024;      private static int current = 1;        private int index;      public DemoTask(int index) {          this.index = index;      }        @Override      public void run() {          while (current <= MAX) {              synchronized (lock) {                  if ((current <= MAX) && (current % 3 == index)) {                      System.out.println(current++);                  }              }          }      }        public static void main(String[] args) throws Exception {          List<Thread> threadList = Arrays.asList(                  new Thread(new DemoTask(0)),                  new Thread(new DemoTask(1)),                  new Thread(new DemoTask(2))          );            threadList.forEach(Thread::start);          for (Thread thread : threadList) {              thread.join();          }      }  }

協同型列印

多個執行緒協同型列印,優勢是各個執行緒使用「通知」機制進行協同分工,理論上執行效率較高,不過要使用對應的「通知」機制。關於如何「通知」,第一種是可使用Java對象的 wait/notify 或者Conditon對象的 await/signal ,第二種是以事件或者提交任務的方式(比如通過提交「待列印數字」這個任務給下一個執行緒)。

下面以第二種方式進行程式碼分析,比如當前執行緒通過submit給下一個執行緒一個「待列印數字」的任務,這樣很容易想到使用只包含1個執行緒的執行緒池來實現,示例程式碼如下:

public class DemoTask implements Runnable {        // 主執行緒要等待執行緒列印完畢,使用CountDownLatch通知機制      private static CountDownLatch countDownLatch = new CountDownLatch(1);      private static List<ExecutorService> threadList = new ArrayList<>();      private static final int MAX = 1024;      private static int current = 1;        @Override      public void run() {          if (current <= MAX) {              System.out.println(current++);              threadList.get(current % threadList.size()).submit(new DemoTask());          } else {              countDownLatch.countDown();          }      }        public static void main(String[] args) throws InterruptedException {          for (int i = 0; i < 3; i++) {              threadList.add(Executors.newFixedThreadPool(1));          }            threadList.get(0).submit(new DemoTask());          countDownLatch.await();          threadList.forEach(ExecutorService::shutdown);      }  }