## 05 看若兄弟,實如父子—Thread和Runnable詳解
> 我們有力的道德就是通過奮斗取得物質上的成功;這種道德既適用于國家,也適用于個人。
> ——羅素
上篇文章,我們學習了Java中實現多線程的兩種基本方式:繼承Thread類和實現Runnable接口。從實現的編程手法來看,認為這是兩種實現方式并無不妥。但是究其實現根源,這么講其實并不準確。在本篇文章中,我們將撤底搞懂這兩種實現方式。
相信大家之前已經對多線程的實現方式爛熟于心:繼承Thread和實現Runnable接口,這么聽起來好像兩種實現方式是并列關系,就像文章標題所講的–“看若兄弟”。但其實多線程從根本上講只有一種實現方式,就是實例化Thread,并且提供其執行的run方法。無論你是通過繼承thread還是實現runnable接口,最終都是重寫或者實現了run方法。而你真正啟動線程都是通過實例化Thread,調用其start方法。我們看下前文中不同實現方式的例子:
1、繼承thread方式
~~~java
Student xiaoming = new Student("小明",punishment);
xiaoming.start();
~~~
2、實現runnable方式
~~~java
Thread xiaoming = new Thread(new Student("小明",punishment),"小明");
xiaoming.start();
~~~
第一種方式中,Student繼承了Thread類,啟動時調用的start方法,其實還是他父類Thread的start方法。并最終觸發執行Student重寫的run方法。
第二種方式中,Student實現Runnable接口,作為參數傳遞給Thread構造函數。接下來還是調用了Thread的start方法。最后則會觸發傳入的runnable實現的run方法。
兩種方式都是創建 Thread 或者 Thread 的子類,通過 Thread 的 start 方法啟動。唯一不同是第一種 run 方法實現在 Thread 子類中。第二種則是把run方法邏輯轉移到 Runnable 的實現類中。線程啟動后,第一種方式是 thread 對象運行自己的 run 方法邏輯,第二種方式則是調用 Runnable 實現的 run 方法邏輯。如下圖所示:
相比較來說,第二種方式是更好的實踐,原因如下:
1. java語言中只能單繼承,通過實現接口的方式,可以讓實現類去繼承其它類。而直接繼承thread就不能再繼承其它類了;
2. 線程控制邏輯在Thread類中,業務運行邏輯在Runnable實現類中。解耦更為徹底;
3. 實現Runnable的實例,可以被多個線程共享并執行。而實現thread是做不到這一點的。
看到這里,你是不是很好奇,為什么程序中調用的是Thread的start方法,而不是run方法?為什么線程在調用start方法后會執行run方法的邏輯呢?接下來我們通過開始start方法的源代碼來找到答案。
## Thread start方法源代碼分析
我們先看Thread類start方法源代碼,如下:
~~~java
public synchronized void start() {
if (threadStatus != 0)
throw new IllegalThreadStateException();
group.add(this);
boolean started = false;
try {
start0();
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
}
}
}
~~~
這段代碼足夠簡單,簡單到沒什么內容。主要邏輯如下:
1. 檢查線程的狀態,是否可以啟動;
2. 把線程加入到線程group中;
3. 調用了start0()方法。
可以看到Start方法中最終調用的是start0方法,并不是run方法。那么我們再看start0方法源代碼:
~~~java
private native void start0();
~~~
什么也沒有,因為start0是一個native方法,也稱為JNI(Java Native Interface)方法。JNI方法是java和其它語言交互的方式。同樣也是java代碼和虛擬機交互的方式,虛擬機就是由C++和匯編所編寫。
由于start0是一個native方法,所以后面的執行會進入到JVM中。那么run方法到底是何時被調用的呢?這里似乎找不到答案了。
難道我們錯過了什么?回過頭來我們再看看Start方法的注解。其實讀源代碼的時候,要先讀注解,否則直接進入代碼邏輯,容易陷進去,出不來。原來答案就在start方法的注解里,我們可以看到:
~~~java
* Causes this thread to begin execution; the Java Virtual Machine
* calls the <code>run</code> method of this thread.
* <p>
* The result is that two threads are running concurrently: the
* current thread (which returns from the call to the
* <code>start</code> method) and the other thread (which executes its
* <code>run</code> method).
* <p>
* It is never legal to start a thread more than once.
* In particular, a thread may not be restarted once it has completed
* execution.
~~~
最關鍵一句\*the Java Virtual Machine calls the run method of this thread。\*由此我們可以推斷出整個執行流程如下:
start方法調用了start0方法,start0方法在JVM中,start0中的邏輯會調用run方法。
至此,我們已經分析清楚從線程創建到run方法被執行的邏輯。但是通過實現Runnbale的方式實現多線程時,Runnable的run方法是如何被調用的呢?
## Thread Run方法分析
對于上面提出的問題,我們先從Thread的構造函數入手。原因是Runnable的實現對象通過構造函數傳入Thread。
~~~java
public Thread(Runnable target) {
init(null, target, "Thread-" + nextThreadNum(), 0);
}
~~~
可以看到Runnable實現作為target對象傳遞進來。再次調用了init方法,init 方法有多個重載,最終調用的是如下方法:
~~~java
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
boolean inheritThreadLocals)
~~~
此方法里有一行代碼:
~~~java
this.target = target;
~~~
原來target是Thread的成員變量:
~~~java
/* What will be run. */
private Runnable target;
~~~
此時,Thread的target被設置為你實現業務邏輯的Runnable實現。
我們再看下run方法的代碼:
~~~java
@Override
public void run() {
if (target != null) {
target.run();
}
}
~~~
看到這里是不是已經很清楚了,當你傳入了target,則會執行target的run方法。也就是執行你實現業務邏輯的方法。整體執行流程如下:
如果你是通過繼承Thread,重寫run方法的方式實現多線程。那么在第三步執行的就是你重寫的run方法。
我們回過頭看看Thread類的定義:
~~~java
public class Thread implements Runnable
~~~
原來Thread也實現了Runnable接口。怪不得Thread類的run方法上有@Override注解。所以繼承thread類實現多線程,其實也相當于是實現runnable接口的run方法。只不過此時,不需要再傳入一個Thread類去啟動。它自己已具備了thread的功能,自己就可以運轉起來。既然Thread類也實現了Runnable接口,那么thread子類對象是不是也可以傳入另外的thread對象,讓其執行自己的run方法呢?答案是可行的,你可以親手試一下。
## 總結
至此,我們已經從理論到代碼,把多線程的兩種實現方式做了分析。在學習多線程的同時,我們也應該學習源代碼中優秀的設計模式。Java中多線程的實現采用了模版模式。Thread是模版對象,負責線程相關的邏輯,比如線程的創建、運行以及各種操作。而線程真正的業務邏輯則被剝離出來,交由Runnable的實現類去實現。線程操作和業務邏輯完全解耦,普通開發者只需要聚焦在業務邏輯實現。
執行業務邏輯,是Thread對象的生命周期中的重要一環。這一步通過調用傳入Runnable的run方法實現。thread線程整體邏輯就是一個模版,把其中一個步驟剝離出來由其他類實現,這就是模版方法模式。講到這里,我們回到標題–“實如父子”。沒錯,其實線程自身的邏輯都在thread類中,而runnable實現類只是線程執行流程中的一小步而已。所以Thread和Runnable更像是父子關系。
講到最后,拋出一個問題,你還記得start方法中如下兩行代碼嗎?
~~~java
if (threadStatus != 0)
throw new IllegalThreadStateException();
~~~
這段代碼判斷了線程狀態屬性threadStatus的值。如果不是0,則直接拋出異常,不會向下執行start0方法。那么Thread有幾種狀態,幾種狀態之間是如何轉換的呢?start之后,run方法立即就會被調用嗎?在下一篇專欄中,將會一一為你解答。
- 前言
- 第1章 Java并發簡介
- 01 開篇詞:多線程為什么是你必需要掌握的知識
- 02 絕對不僅僅是為了面試—我們為什么需要學習多線程
- 03 多線程開發如此簡單—Java中如何編寫多線程程序
- 04 人多力量未必大—并發可能會遇到的問題
- 第2章 Java中如何編寫多線程
- 05 看若兄弟,實如父子—Thread和Runnable詳解
- 06 線程什么時候開始真正執行?—線程的狀態詳解
- 07 深入Thread類—線程API精講
- 08 集體協作,什么最重要?溝通!—線程的等待和通知
- 09 使用多線程實現分工、解耦、緩沖—生產者、消費者實戰
- 第3章 并發的問題和原因詳解
- 10 有福同享,有難同當—原子性
- 11 眼見不實—可見性
- 12 什么?還有這種操作!—有序性
- 13 問題的根源—Java內存模型簡介
- 14 僵持不下—死鎖詳解
- 第4章 如何解決并發問題
- 15 原子性輕量級實現—深入理解Atomic與CAS
- 16 讓你眼見為實—volatile詳解
- 17 資源有限,請排隊等候—Synchronized使用、原理及缺陷
- 18 線程作用域內共享變量—深入解析ThreadLocal
- 第5章 線程池
- 19 自己動手豐衣足食—簡單線程池實現
- 20 其實不用造輪子—Executor框架詳解
- 第6章 主要并發工具類
- 21 更高級的鎖—深入解析Lock
- 22 到底哪把鎖更適合你?—synchronized與ReentrantLock對比
- 23 按需上鎖—ReadWriteLock詳解
- 24 經典并發容器,多線程面試必備—深入解析ConcurrentHashMap上
- 25 經典并發容器,多線程面試必備—深入解析ConcurrentHashMap下
- 26不讓我進門,我就在門口一直等!—BlockingQueue和ArrayBlockingQueue
- 27 倒數計時開始,三、二、一—CountDownLatch詳解
- 28 人齊了,一起行動—CyclicBarrier詳解
- 29 一手交錢,一手交貨—Exchanger詳解
- 30 限量供應,不好意思您來晚了—Semaphore詳解
- 第7章 高級并發工具類及并發設計模式
- 31 憑票取餐—Future模式詳解
- 32 請按到場順序發言—Completion Service詳解
- 33 分階段執行你的任務-學習使用Phaser運行多階段任務
- 34 誰都不能偷懶-通過 CompletableFuture 組裝你的異步計算單元
- 35 拆分你的任務—學習使用Fork/Join框架
- 36 為多線程們安排一位經理—Master/Slave模式詳解
- 第8章 總結
- 37 結束語