1. 程式人生 > >面試官問我:建立執行緒有幾種方式?我笑了

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

## 前言 多執行緒在面試中基本上已經是必問項了,面試官通常會從簡單的問題開始發問,然後再一步一步的挖掘你的知識面。 比如,從執行緒是什麼開始,執行緒和程序的區別,建立執行緒有幾種方式,執行緒有幾種狀態,等等。 接下來自然就會引出執行緒池,Lock,Synchronized,JUC的各種併發包。然後就會引出 AQS、CAS、JMM、JVM等偏底層原理,一環扣一環。 這一節我們不聊其他的,只說建立執行緒有幾種方式。 是不是感覺非常簡單,不就是那個啥啥那幾種麼。 其實不然,只有我們給面試官解釋清楚了,並加上我們自己的理解,才能在面試中加分。 ## 正文 一般來說我們比較常用的有以下四種方式,下面先介紹它們的使用方法。然後,再說面試中怎樣回答面試官的問題比較合適。 ### 1、繼承 Thread 類 通過繼承 Thread 類,並重寫它的 run 方法,我們就可以建立一個執行緒。 * 首先定義一個類來繼承 Thread 類,重寫 run 方法。 * 然後建立這個子類物件,並呼叫 start 方法啟動執行緒。 ![](https://img2020.cnblogs.com/other/1714084/202010/1714084-20201024134631779-1386218468.png) ### 2、實現 Runnable 介面 通過實現 Runnable ,並實現 run 方法,也可以建立一個執行緒。 * 首先定義一個類實現 Runnable 介面,並實現 run 方法。 * 然後建立 Runnable 實現類物件,並把它作為 target 傳入 Thread 的建構函式中 * 最後呼叫 start 方法啟動執行緒。 ![](https://img2020.cnblogs.com/other/1714084/202010/1714084-20201024134632084-1245264276.png) ### 3、實現 Callable 介面,並結合 Future 實現 * 首先定義一個 Callable 的實現類,並實現 call 方法。call 方法是帶返回值的。 * 然後通過 FutureTask 的構造方法,把這個 Callable 實現類傳進去。 * 把 FutureTask 作為 Thread 類的 target ,建立 Thread 執行緒物件。 * 通過 FutureTask 的 get 方法獲取執行緒的執行結果。 ![](https://img2020.cnblogs.com/other/1714084/202010/1714084-20201024134632394-2046627540.png) ### 4、通過執行緒池建立執行緒 此處用 JDK 自帶的 Executors 來建立執行緒池物件。 * 首先,定一個 Runnable 的實現類,重寫 run 方法。 * 然後建立一個擁有固定執行緒數的執行緒池。 * 最後通過 ExecutorService 物件的 execute 方法傳入執行緒物件。 ![](https://img2020.cnblogs.com/other/1714084/202010/1714084-20201024134632693-1369439361.png) ### 到底有幾種建立執行緒的方式? 那麼問題來了,我這裡舉例了四種建立執行緒的方式,是不是說明就是四種呢? 我們先看下 JDK 原始碼中對 Thread 類的一段解釋,如下圖。 ![](https://img2020.cnblogs.com/other/1714084/202010/1714084-20201024134632974-1533151803.png) > There are two ways to create a new thread of execution > > 翻譯: 有兩種方式可以建立一個新的執行執行緒 這裡說的兩種方式就對應我們介紹的前兩種方式。 但是,我們會發現這兩種方式,最終都會呼叫 Thread.start 方法,而 start 方法最終會呼叫 run 方法。 不同的是,在實現 Runnable 介面的方式中,呼叫的是 Thread 本類的 run 方法。我們看下它的原始碼, ![](https://img2020.cnblogs.com/other/1714084/202010/1714084-20201024134633198-1669139621.png) 這種方式,會把建立的 Runnable 實現類物件賦值給 target ,並執行 target 的 run 方法。 再看繼承 Thread 類的方式,我們同樣需要呼叫 Thread 的 start 方法來啟動執行緒。由於子類重寫了 Thread 類的 run 方法,因此最終執行的是這個子類的 run 方法。 所以,我們也可以這樣說。在本質上,建立執行緒只有一種方式,就是構造一個 Thread 類(其子類其實也可以認為是一個 Thread 類)。 而構造 Thread 類又有兩種方式,一種是繼承 Thread 類,一種是實現 Runnable介面。其最終都會建立 Thread 類(或其子類)的物件。 再來看實現 Callable ,結合 Future 和 FutureTask 的方式。可以發現,其最終也是通過 new Thread(task) 的方式構造 Thread 類。 最後,線上程池中,我們其實是把建立和管理執行緒的任務都交給了執行緒池。而建立執行緒是通過執行緒工廠類 DefaultThreadFactory 來建立的(也可以自定義工廠類)。我們看下這個工廠類的具體實現。 ![](https://img2020.cnblogs.com/other/1714084/202010/1714084-20201024134633428-961731645.png) 它會給執行緒設定一些預設值,如執行緒名稱,執行緒的優先順序,執行緒組,是否是守護執行緒等。最後還是通過 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 本身就有四個不同引數的建構函式,如下圖, ![](G:\typoraImages\image-20201023203426961.png) 有四種方式可以建立 HashMap 物件,除此之外,還可以通過 ### 有趣的題目 問:一個類實現了 Runnable 介面就會執行預設的 run 方法,然後判斷 target 不為空,最後執行在 Runnable介面中實現的 run 方法。而繼承 Thread 類,就會執行重寫後的 run 方法。那麼,現在我既繼承 Thread 類,又實現 Runnable 介面,如下程式,應該輸出什麼結果呢? ```java 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。 所以,上邊的程式碼看起來複雜,實則很簡單。透過現象看本質,我們就會發現,它不過就是考察類的父子繼承關係,子類重寫了父類的方法就會優先執行子類重寫的方法。 和執行緒結合起來,如果對執行緒執行機制不熟悉的,很可能就會被