面試官問我:創建線程有幾種方式?我笑了


前言

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

比如,從線程是什么開始,線程和進程的區別,創建線程有幾種方式,線程有幾種狀態,等等。

接下來自然就會引出線程池,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 並發專欄)

個人想法

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

個人認為,如果你要說有 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 類,你抓狂不。

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

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

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

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

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

所以個人認為,說創建線程只有一種方式有些欠妥。

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

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

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

有趣的題目

問:一個類實現了 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。

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

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM