前言
多線程在面試中基本上已經是必問項了,面試官通常會從簡單的問題開始發問,然后再一步一步的挖掘你的知識面。
比如,從線程是什么開始,線程和進程的區別,創建線程有幾種方式,線程有幾種狀態,等等。
接下來自然就會引出線程池,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 有幾種方式。你八成會為了炫耀自己知道的多,跟我說,
- 通過構造方法,
List list = new ArrayList();
- 通過
Arrays.asList("a", "b")
; - 通過Java8提供的Stream API,如
List list = Stream.of("a", "b").collect(Collectors.toList());
- 通過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。
所以,上邊的代碼看起來復雜,實則很簡單。透過現象看本質,我們就會發現,它不過就是考察類的父子繼承關系,子類重寫了父類的方法就會優先執行子類重寫的方法。
和線程結合起來,如果對線程運行機制不熟悉的,很可能就會被迷惑。