在本課時我們主要學習為什么說本質上只有一種實現線程的方式?實現 Runnable 接口究竟比繼承 Thread 類實現線程好在哪里?
實現線程是并發編程中基礎中的基礎,因為我們必須要先實現多線程,才可以繼續后續的一系列操作。所以本課時就先從并發編程的基礎如何實現線程開始講起,希望你能夠夯實基礎,雖然實現線程看似簡單、基礎,但實際上卻暗藏玄機。首先,我們來看下為什么說本質上實現線程只有一種方式?
實現線程的方式到底有幾種?大部分人會說有 2 種、3 種或是 4 種,很少有人會說有 1 種。我們接下來看看它們具體指什么?2 種實現方式的描述是最基本的,也是最為大家熟知的,我們就先來看看 2 種線程實現方式的源碼。
### 實現 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?{
?
????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<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?{
}
~~~
講到這里你可能會說,我還知道一些其他的實現線程的方式。比如,定時器也可以實現線程,如果新建一個 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 類實現的。
接下來,我們進行更深層次的探討,為什么說這兩種方式本質上是一種呢?
復制
~~~
@Override
public?void?run()?{
????if?(target?!=?null)?{
????????target.run();
????}
}
~~~
首先,啟動線程需要調用 start() 方法,而 start() 方法最終還會調用 run() 方法,我們先來看看第一種方式中 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 類實現線程好在哪里?學習完本課時相信你一定對創建線程有了更深入的理解。
[https://kaiwu.lagou.com/course/courseInfo.htm?courseId=16#/detail/pc?id=239](https://kaiwu.lagou.com/course/courseInfo.htm?courseId=16#/detail/pc?id=239)
- 線程基礎升華
- 第01講:為何說只有 1 種實現線程的方法?
- 第02講:如何正確停止線程?為什么 volatile 標記位的停止方法是錯誤的?
- 第03講:線程是如何在 6 種狀態之間轉換的?
- 第04講:wait/notify/notifyAll 方法的使用注意事項?
- 第05講:有哪幾種實現生產者消費者模式的方法?
- 究竟什么是線程安全?
- 第06講:一共有哪 3 類線程安全問題?
- 第07講:哪些場景需要額外注意線程安全問題?
- 第08講:為什么多線程會帶來性能問題?
- 線程池
- 第09講:使用線程池比手動創建線程好在哪里?
- 第10講:線程池的各個參數的含義?
- 第11講:線程池有哪 4 種拒絕策略?
- 第12講:有哪 6 種常見的線程池?什么是 Java8 的 ForkJoinPool?
- 第13講:線程池常用的阻塞隊列有哪些?
- 第14講:為什么不應該自動創建線程池?
- 第15講:合適的線程數量是多少?CPU 核心數和線程數的關系?
- 第16講:如何根據實際需要,定制自己的線程池?
- 第17講:如何正確關閉線程池?shutdown 和 shutdownNow 的區別?
- 第18講:線程池實現“線程復用”的原理?
- 各種各樣的“鎖”
- 第19講:你知道哪幾種鎖?分別有什么特點?
- 第20講:悲觀鎖和樂觀鎖的本質是什么?
- 第21講:如何看到 synchronized 背后的“monitor 鎖”?
- 第22講:synchronized 和 Lock 孰優孰劣,如何選擇?
- 第23講:Lock 有哪幾個常用方法?分別有什么用?
- 第24講:講一講公平鎖和非公平鎖,為什么要“非公平”?
- 第25講:讀寫鎖 ReadWriteLock 獲取鎖有哪些規則?
- 第26講:讀鎖應該插隊嗎?什么是讀寫鎖的升降級?
- 第27講:什么是自旋鎖?自旋的好處和后果是什么呢?
- 第28講:JVM 對鎖進行了哪些優化?
- 并發容器面面觀
- 第29講:HashMap 為什么是線程不安全的?
- 第30講:ConcurrentHashMap 在 Java7 和 8 有何不同?
- 第31講:為什么 Map 桶中超過 8 個才轉為紅黑樹?
- 第32講:同樣是線程安全,ConcurrentHashMap 和 Hashtable 的區別?
- 第33講:CopyOnWriteArrayList 有什么特點?
- 阻塞隊列
- 第34講:什么是阻塞隊列?
- 第35講:阻塞隊列包含哪些常用的方法?add、offer、put 等方法的區別?
- 第36講:有哪幾種常見的阻塞隊列?
- 第37講:阻塞和非阻塞隊列的并發安全原理是什么?
- 第38講:如何選擇適合自己的阻塞隊列?
- 原子類
- 第39講:原子類是如何利用 CAS 保證線程安全的?
- 第40講:AtomicInteger 在高并發下性能不好,如何解決?為什么?
- 第41講:原子類和 volatile 有什么異同?
- 第42講:AtomicInteger 和 synchronized 的異同點?
- TheadLocal
- 第44講:ThreadLocal 適合用在哪些實際生產的場景中?
- 第45講:ThreadLocal 是用來解決共享資源的多線程訪問的問題嗎?
- 第46講:多個 ThreadLocal 在 Thread 中的 threadlocals 里是怎么存儲的?
- 第47講:內存泄漏——為何每次用完 ThreadLocal 都要調用 remove()?
- Future 掌控未來
- 第48講:Callable 和 Runnable 的不同?
- 第49講:Future 的主要功能是什么?
- 第50講:使用 Future 有哪些注意點?Future 產生新的線程了嗎?
- 線程協作
- 第52講:信號量能被 FixedThreadPool 替代嗎?
- 第53講:CountDownLatch 是如何安排線程執行順序的?
- 第54講:CyclicBarrier 和 CountdownLatch 有什么異同?
- 第55講:Condition、object.wait() 和 notify() 的關系?
- Java 內存模型
- 第56講:講一講什么是 Java 內存模型?
- 第57講:什么是指令重排序?為什么要重排序?
- 第58講:Java 中的原子操作有哪些注意事項?
- 第59講:什么是“內存可見性”問題?
- 第60講:主內存和工作內存的關系?
- 第61講:什么是 happens-before 規則?
- 第62講:volatile 的作用是什么?與 synchronized 有什么異同?
- 第63講:單例模式的雙重檢查鎖模式為什么必須加 volatile?
- CAS 原理
- 第64講:你知道什么是 CAS 嗎?
- 第65講:CAS 和樂觀鎖的關系,什么時候會用到 CAS?
- 第66講:CAS 有什么缺點?
- 死鎖問題
- 第67講:如何寫一個必然死鎖的例子?
- 第68講:發生死鎖必須滿足哪 4 個條件?
- 第69講:如何用命令行和代碼定位死鎖?
- 第70講:有哪些解決死鎖問題的策略?
- 第71講:講一講經典的哲學家就餐問題
- final 關鍵字和“不變性”
- 第72講:final 的三種用法是什么?
- 第73講:為什么加了 final 卻依然無法擁有“不變性”?
- 第74講:為什么 String 被設計為是不可變的?
- AQS 框架
- 第75講:為什么需要 AQS?AQS 的作用和重要性是什么?
- 第76講:AQS 的內部原理是什么樣的?
- 第77講:AQS 在 CountDownLatch 等類中的應用原理是什么?
- 總結
- 第78講:一份獨家的 Java 并發工具圖譜