面試官問我:建立執行緒有幾種方式?我笑了

煙雨星空發表於2020-10-24

前言

多執行緒在面試中基本上已經是必問項了,面試官通常會從簡單的問題開始發問,然後再一步一步的挖掘你的知識面。

比如,從執行緒是什麼開始,執行緒和程式的區別,建立執行緒有幾種方式,執行緒有幾種狀態,等等。

接下來自然就會引出執行緒池,Lock,Synchronized,JUC的各種併發包。然後就會引出 AQS、CAS、JMM、JVM等偏底層原理,一環扣一環。

這一節我們不聊其他的,只說建立執行緒有幾種方式。

是不是感覺非常簡單,不就是那個啥啥那幾種麼。

其實不然,只有我們給面試官解釋清楚了,並加上我們自己的理解,才能在面試中加分。

正文

一般來說我們比較常用的有以下四種方式,下面先介紹它們的使用方法。然後,再說面試中怎樣回答面試官的問題比較合適。

1、繼承 Thread 類

通過繼承 Thread 類,並重寫它的 run 方法,我們就可以建立一個執行緒。

  • 首先定義一個類來繼承 Thread 類,重寫 run 方法。
  • 然後建立這個子類物件,並呼叫 start 方法啟動執行緒。

2、實現 Runnable 介面

通過實現 Runnable ,並實現 run 方法,也可以建立一個執行緒。

  • 首先定義一個類實現 Runnable 介面,並實現 run 方法。
  • 然後建立 Runnable 實現類物件,並把它作為 target 傳入 Thread 的建構函式中
  • 最後呼叫 start 方法啟動執行緒。

3、實現 Callable 介面,並結合 Future 實現

  • 首先定義一個 Callable 的實現類,並實現 call 方法。call 方法是帶返回值的。
  • 然後通過 FutureTask 的構造方法,把這個 Callable 實現類傳進去。
  • 把 FutureTask 作為 Thread 類的 target ,建立 Thread 執行緒物件。
  • 通過 FutureTask 的 get 方法獲取執行緒的執行結果。

4、通過執行緒池建立執行緒

此處用 JDK 自帶的 Executors 來建立執行緒池物件。

  • 首先,定一個 Runnable 的實現類,重寫 run 方法。
  • 然後建立一個擁有固定執行緒數的執行緒池。
  • 最後通過 ExecutorService 物件的 execute 方法傳入執行緒物件。

到底有幾種建立執行緒的方式?

那麼問題來了,我這裡舉例了四種建立執行緒的方式,是不是說明就是四種呢?

我們先看下 JDK 原始碼中對 Thread 類的一段解釋,如下圖。

There are two ways to create a new thread of execution

翻譯: 有兩種方式可以建立一個新的執行執行緒

這裡說的兩種方式就對應我們介紹的前兩種方式。

但是,我們會發現這兩種方式,最終都會呼叫 Thread.start 方法,而 start 方法最終會呼叫 run 方法。

不同的是,在實現 Runnable 介面的方式中,呼叫的是 Thread 本類的 run 方法。我們看下它的原始碼,

這種方式,會把建立的 Runnable 實現類物件賦值給 target ,並執行 target 的 run 方法。

再看繼承 Thread 類的方式,我們同樣需要呼叫 Thread 的 start 方法來啟動執行緒。由於子類重寫了 Thread 類的 run 方法,因此最終執行的是這個子類的 run 方法。

所以,我們也可以這樣說。在本質上,建立執行緒只有一種方式,就是構造一個 Thread 類(其子類其實也可以認為是一個 Thread 類)。

而構造 Thread 類又有兩種方式,一種是繼承 Thread 類,一種是實現 Runnable介面。其最終都會建立 Thread 類(或其子類)的物件。

再來看實現 Callable ,結合 Future 和 FutureTask 的方式。可以發現,其最終也是通過 new Thread(task) 的方式構造 Thread 類。

最後,線上程池中,我們其實是把建立和管理執行緒的任務都交給了執行緒池。而建立執行緒是通過執行緒工廠類 DefaultThreadFactory 來建立的(也可以自定義工廠類)。我們看下這個工廠類的具體實現。

它會給執行緒設定一些預設值,如執行緒名稱,執行緒的優先順序,執行緒組,是否是守護執行緒等。最後還是通過 new Thread() 的方式來建立執行緒的。

因此,綜上所述。在回答這個問題的時候,我們可以說本質上建立執行緒就只有一種方式,就是構造一個 Thread 類。(此結論借鑑來源於 Java 併發程式設計 78 講 -- 徐隆曦)

個人想法

但是,在這裡我想對這個結論稍微提出一些疑問(若有不同見解,文末可留言交流~)。。。

個人認為,如果你要說有 1種、2種、3種、4種 其實也是可以的。重要的是,你要能說出你的依據,講出它們各自的不同點和共同點。講得頭頭是道,讓面試官對你頻頻點頭。。

說只有構造 Thread 類這一種建立執行緒方式,個人認為還是有些牽強。因為,無論你從任何手段出發,想建立一個執行緒的話,最終肯定都是構造 Thread 類。(包括以上幾種方式,甚至通過反射,最終不也是 newInstance 麼)。

那麼,如果按照這個邏輯的話,我就可以說,不管建立任何的物件(Object),都是隻有一種方式,即構造這個物件(Object) 類。這個結論似乎有些太過無聊了,因為這是一句非常正確的廢話。

以 ArrayList 為例,我問你建立 ArrayList 有幾種方式。你八成會為了炫耀自己知道的多,跟我說,

  1. 通過構造方法,List list = new ArrayList();
  2. 通過 Arrays.asList("a", "b");
  3. 通過Java8提供的Stream API,如 List list = Stream.of("a", "b").collect(Collectors.toList());
  4. 通過guava第三方jar包,List list3 = Lists.newArrayList("a", "b");

等等,僅以上就列舉了四種。現在,我告訴你建立 ArrayList 就只有一種方式,即構造一個 ArrayList 類,你抓狂不。

這就如同,我問你從北京出發到上海去有幾種方式。

你說可以坐汽車、火車、坐動車、坐高鐵,坐飛機。

那不對啊,動車和高鐵都屬於火車啊,汽車和火車都屬於車,車和飛機都屬於交通工具。這樣就是隻有一種方式了,即坐交通工具。

這也不對啊,我不坐交通工具也行啊,我走路過去不行麼(我插眼傳送也可以啊,就你皮~)。

最後結論就是,只有一種方式,那就是你人到上海即可。這這這,這算什麼結論。。。

所以個人認為,說建立執行緒只有一種方式有些欠妥。

好好的一個技術文,差一點被我寫成議論文了。。。

這個仁者見仁智者見智吧。

最後,我們看一下我從網上看到的一個非常有意思的題目。

但是,我們知道 HashMap 本身就有四個不同引數的建構函式,如下圖,

有四種方式可以建立 HashMap 物件,除此之外,還可以通過

有趣的題目

問:一個類實現了 Runnable 介面就會執行預設的 run 方法,然後判斷 target 不為空,最後執行在 Runnable介面中實現的 run 方法。而繼承 Thread 類,就會執行重寫後的 run 方法。那麼,現在我既繼承 Thread 類,又實現 Runnable 介面,如下程式,應該輸出什麼結果呢?

public class TestThread {
    public static void main(String[] args) {
        new Thread(()-> System.out.println("runnable")){
            @Override
            public void run() {
                System.out.println("Thread run");
            }
        }.start();
    }
}

可能乍一看很懵逼,這是什麼操作。

其實,我們拆解一下以上程式碼就會知道,這是一個繼承了 Thread 父類的子類物件,重寫了父類的 run 方法。然後,父物件 Thread 中,在構造方法中傳入了一個 Runnable 介面的實現類,實現了 run 方法。

現在執行了 start 方法,必然會先在子類中尋找 run 方法,找到了就會直接執行,不會執行父類的 run 方法了,因此結果為:Thread run 。

若假設子類沒有實現 run 方法,那麼就會去父類中尋找 run 方法,而父類的 run 方法會判斷是否有 Runnable傳過來(即判斷target是否為空),現在 target 不為空,因此就會執行 target.run 方法,即列印結果: runnable。

所以,上邊的程式碼看起來複雜,實則很簡單。透過現象看本質,我們就會發現,它不過就是考察類的父子繼承關係,子類重寫了父類的方法就會優先執行子類重寫的方法。

和執行緒結合起來,如果對執行緒執行機制不熟悉的,很可能就會被迷惑。

相關文章