實現執行緒的方式到底有幾種?

  • 2020 年 2 月 16 日
  • 筆記

這篇文章主要講解實現執行緒的方式到底有幾種?以及實現 Runnable 介面究竟比繼承 Thread 類實現執行緒好在哪裡?

實現執行緒是並發編程中基礎中的基礎,因為我們必須要先實現多執行緒,才可以繼續後續的一系列操作。所以本文就先從並發編程的基礎如何實現執行緒開始講起。

實現執行緒的方式到底有幾種?我們接下來看看它們具體指什麼?

實現 Runnable 介面

public class RunnableThread implements Runnable {        @Override      public void run() {          System.out.println("實現Runnable介面實現執行緒");      }  }

第 1 種方式是通過實現 Runnable 介面實現多執行緒,如程式碼所示,首先通過 RunnableThread 類實現 Runnable 介面,然後重寫 run() 方法,之後只需要把這個實現了 run() 方法的實例傳到 Thread 類中就可以實現多執行緒。

繼承 Thread 類

public class ExtendsThread extends Thread {        @Override      public void run() {          System.out.println(「繼承Thread類實現執行緒");      }  }

第 2 種方式是繼承 Thread 類,如程式碼所示,與第 1 種方式不同的是它沒有實現介面,而是繼承 Thread 類,並重寫了其中的 run() 方法。相信上面這兩種方式你一定非常熟悉,並且經常在工作中使用它們。

執行緒池創建執行緒

那麼為什麼說還有第 3 種或第 4 種方式呢?我們先來看看第 3 種方式:通過執行緒池創建執行緒。執行緒池確實實現了多執行緒,比如我們給執行緒池的執行緒數量設置成 10,那麼就會有 10 個子執行緒來為我們工作,接下來,我們深入解析執行緒池中的源碼,來看看執行緒池是怎麼實現執行緒的?

static class DefaultThreadFactory implements ThreadFactory {      private static final AtomicInteger poolNumber = new AtomicInteger(1);      private final ThreadGroup group;      private final AtomicInteger threadNumber = new AtomicInteger(1);      private final String namePrefix;        DefaultThreadFactory() {          SecurityManager s = System.getSecurityManager();          group = (s != null) ? s.getThreadGroup() :                                  Thread.currentThread().getThreadGroup();          namePrefix = "pool-" +                          poolNumber.getAndIncrement() +                          "-thread-";      }        public Thread newThread(Runnable r) {          Thread t = new Thread(group, r,                                  namePrefix + threadNumber.getAndIncrement(),                                  0);          if (t.isDaemon())              t.setDaemon(false);          if (t.getPriority() != Thread.NORM_PRIORITY)              t.setPriority(Thread.NORM_PRIORITY);          return t;      }  }

對於執行緒池而言,本質上是通過執行緒工廠創建執行緒的,默認採用 DefaultThreadFactory ,它會給執行緒池創建的執行緒設置一些默認值,比如:執行緒的名字、是否是守護執行緒,以及執行緒的優先順序等。但是無論怎麼設置這些屬性,最終它還是通過 new Thread() 創建執行緒的 ,只不過這裡的構造函數傳入的參數要多一些,由此可以看出通過執行緒池創建執行緒並沒有脫離最開始的那兩種基本的創建方式,因為本質上還是通過 new Thread() 實現的。

除此之外,Callable 也是可以創建執行緒的,但是本質上也是通過前兩種基本方式實現的執行緒創建。

有返回值的 Callable 創建執行緒

class CallableTask implements Callable<Integer> {        @Override      public Integer call() throws Exception {          return new Random().nextInt();      }  }    //創建執行緒池  ExecutorService service = Executors.newFixedThreadPool(10);  //提交任務,並用 Future提交返回結果  Future<Integer> future = service.submit(new CallableTask());

第 4 種執行緒創建方式是通過有返回值的 Callable 創建執行緒,Runnable 創建執行緒是無返回值的,而 Callable 和與之相關的 Future、FutureTask,它們可以把執行緒執行的結果作為返回值返回,如程式碼所示,實現了 Callable 介面,並且給它的泛型設置成 Integer,然後它會返回一個隨機數。

但是,無論是 Callable 還是 FutureTask,它們首先和 Runnable 一樣,都是一個任務,是需要被執行的,而不是說它們本身就是執行緒。它們可以放到執行緒池中執行,如程式碼所示, submit() 方法把任務放到執行緒池中,並由執行緒池創建執行緒,不管用什麼方法,最終都是靠執行緒來執行的,而子執行緒的創建方式仍脫離不了最開始講的兩種基本方式,也就是實現 Runnable 介面和繼承 Thread 類。

除了上述常用的實現執行緒的方式還有以下方式:

定時器 Timer

class TimerThread extends Thread {        boolean newTasksMayBeScheduled = true;        private TaskQueue queue;        TimerThread(TaskQueue queue) {          this.queue = queue;      }        public void run() {          try {              mainLoop();          } finally {              synchronized(queue) {                  newTasksMayBeScheduled = false;                  queue.clear();              }          }      }        private void mainLoop() {          while (true) {              try {                  TimerTask task;                  boolean taskFired;                  synchronized(queue) {                      while (queue.isEmpty() && newTasksMayBeScheduled)                          queue.wait();                      if (queue.isEmpty())                          break;                        long currentTime, executionTime;                      task = queue.getMin();                      synchronized(task.lock) {                          if (task.state == TimerTask.CANCELLED) {                              queue.removeMin();                              continue;                          }                          currentTime = System.currentTimeMillis();                          executionTime = task.nextExecutionTime;                          if (taskFired = (executionTime<=currentTime)) {                              if (task.period == 0) {                                  queue.removeMin();                                  task.state = TimerTask.EXECUTED;                              } else {                                  queue.rescheduleMin(                                    task.period<0 ? currentTime   - task.period                                                  : executionTime + task.period);                              }                          }                      }                      if (!taskFired)                          queue.wait(executionTime - currentTime);                  }                  if (taskFired)                      task.run();              } catch(InterruptedException e) {              }          }      }  }

定時器也可以實現執行緒,如果新建一個 Timer,令其每隔 10 秒或設置兩個小時之後,執行一些任務,那麼這時它確實也創建了執行緒並執行了任務,但如果我們深入分析定時器的源碼會發現,本質上它還是會有一個繼承自 Thread 類的 TimerThread,所以定時器創建執行緒最後又繞回到最開始說的兩種方式。

匿名內部類創建執行緒

new Thread(new Runnable() {      @Override      public void run() {          System.out.println(Thread.currentThread().getName());      }  }).start();

還有通過匿名內部類或 lambda 表達式方式來創建執行緒,實際上,匿名內部類或 lambda 表達式創建執行緒,它們僅僅是在語法層面上實現了執行緒,並不能把它歸結於實現多執行緒的方式,如匿名內部類實現執行緒的程式碼所示,它僅僅是用一個匿名內部類把需要傳入的 Runnable 給實例出來。

new Thread(() -> System.out.println(Thread.currentThread().getName())).start();

我們再來看下 lambda 表達式方式。如程式碼所示,最終它們依然符合最開始所說的那兩種實現執行緒的方式。

實現執行緒只有一種方式

我們先不認為創建執行緒只有一種方式,先認為有兩種創建執行緒的方式,而其他的創建方式,比如執行緒池或是定時器,它們僅僅是在 new Thread() 外做了一層封裝,如果我們把這些都叫作一種新的方式,那麼創建執行緒的方式便會千變萬化、層出不窮,比如 JDK 更新了,它可能會多出幾個類,會把 new Thread() 重新封裝,表面上看又會是一種新的實現執行緒的方式,透過現象看本質,打開封裝後,會發現它們最終都是基於 Runnable 介面或繼承 Thread 類實現的。

接下來,我們進行更深層次的探討,為什麼說這兩種方式本質上是一種呢?

首先,啟動執行緒需要調用 start() 方法,而 start() 方法最終還會調用 run() 方法,我們先來看看第一種方式中 run() 方法究竟是怎麼實現的:

@Override  public void run() {      if (target != null) {          target.run();      }  }

可以看出 run() 方法的程式碼非常短小精悍,第 1 行程式碼 if (target != null) ,判斷 target 是否等於 null,如果不等於 null,就執行第 2 行程式碼 target.run(),而 target 實際上就是一個 Runnable,即使用 Runnable 介面實現執行緒時傳給 Thread 類的對象。

然後,我們來看第二種方式,也就是繼承 Thread 方式,實際上,繼承 Thread 類之後,會把上述的 run() 方法重寫,重寫後 run() 方法里直接就是所需要執行的任務,但它最終還是需要調用 thread.start() 方法來啟動執行緒,而 start() 方法最終也會調用這個已經被重寫的 run() 方法來執行它的任務,這時我們就可以徹底明白了,事實上創建執行緒只有一種方式,就是構造一個 Thread 類,這是創建執行緒的唯一方式。

我們上面已經了解了兩種創建執行緒方式本質上是一樣的,它們的不同點僅僅在於實現執行緒運行內容的不同,那麼運行內容來自於哪裡呢?

運行內容主要來自於兩個地方,要麼來自於 target,要麼來自於重寫的 run() 方法,在此基礎上我們進行拓展,可以這樣描述:本質上,實現執行緒只有一種方式,而要想實現執行緒執行的內容,卻有兩種方式,也就是可以通過實現 Runnable 介面的方式,或是繼承 Thread 類重寫 run() 方法的方式,把我們想要執行的程式碼傳入,讓執行緒去執行,在此基礎上,如果我們還想有更多實現執行緒的方式,比如執行緒池和 Timer 定時器,只需要在此基礎上進行封裝即可。

實現 Runnable 介面比繼承 Thread 類實現執行緒要好

下面我們來對剛才說的兩種實現執行緒內容的方式進行對比,也就是為什麼說實現 Runnable 介面比繼承 Thread 類實現執行緒要好?好在哪裡呢?

首先,我們從程式碼的架構考慮,實際上,Runnable 里只有一個 run() 方法,它定義了需要執行的內容,在這種情況下,實現了 Runnable 與 Thread 類的解耦,Thread 類負責執行緒啟動和屬性設置等內容,權責分明。

第二點就是在某些情況下可以提高性能,使用繼承 Thread 類方式,每次執行一次任務,都需要新建一個獨立的執行緒,執行完任務後執行緒走到生命周期的盡頭被銷毀,如果還想執行這個任務,就必須再新建一個繼承了 Thread 類的類,如果此時執行的內容比較少,比如只是在 run() 方法里簡單列印一行文字,那麼它所帶來的開銷並不大,相比於整個執行緒從開始創建到執行完畢被銷毀,這一系列的操作比 run() 方法列印文字本身帶來的開銷要大得多,相當於撿了芝麻丟了西瓜,得不償失。如果我們使用實現 Runnable 介面的方式,就可以把任務直接傳入執行緒池,使用一些固定的執行緒來完成任務,不需要每次新建銷毀執行緒,大大降低了性能開銷。

第三點好處在於 Java 語言不支援雙繼承,如果我們的類一旦繼承了 Thread 類,那麼它後續就沒有辦法再繼承其他的類,這樣一來,如果未來這個類需要繼承其他類實現一些功能上的拓展,它就沒有辦法做到了,相當於限制了程式碼未來的可拓展性。

綜上所述,我們應該優先選擇通過實現 Runnable 介面的方式來創建執行緒。

總結

本文主要學習了通過 Runnable 介面和繼承 Thread 類等幾種方式創建執行緒,又詳細分析了為什麼說本質上只有一種實現執行緒的方式,以及實現 Runnable 介面究竟比繼承 Thread 類實現執行緒好在哪裡?看完本文相信你一定對創建執行緒有了更深入的理解。