今天我們來深入聊聊線程,相信大家對于線程這個概念都不陌生,它是 Java 并發的基礎元素,理解、操縱、診斷線程是 Java 工程師的必修課,但是你真的掌握線程了嗎?
今天我要問你的問題是,一個線程兩次調用 start() 方法會出現什么情況?談談線程的生命周期和狀態轉移。
## 典型回答
Java 的線程是不允許啟動兩次的,第二次調用必然會拋出 IllegalThreadStateException,這是一種運行時異常,多次調用 start 被認為是編程錯誤。
關于線程生命周期的不同狀態,在 Java 5 以后,線程狀態被明確定義在其公共內部枚舉類型 java.lang.Thread.State 中,分別是:
* 新建(NEW),表示線程被創建出來還沒真正啟動的狀態,可以認為它是個 Java 內部狀態。
* 就緒(RUNNABLE),表示該線程已經在 JVM 中執行,當然由于執行需要計算資源,它可能是正在運行,也可能還在等待系統分配給它 CPU 片段,在就緒隊列里面排隊。
* 在其他一些分析中,會額外區分一種狀態 RUNNING,但是從 Java API 的角度,并不能表示出來。
* 阻塞(BLOCKED),這個狀態和我們前面兩講介紹的同步非常相關,阻塞表示線程在等待 Monitor lock。比如,線程試圖通過 synchronized 去獲取某個鎖,但是其他線程已經獨占了,那么當前線程就會處于阻塞狀態。
* 等待(WAITING),表示正在等待其他線程采取某些操作。一個常見的場景是類似生產者消費者模式,發現任務條件尚未滿足,就讓當前消費者線程等待(wait),另外的生產者線程去準備任務數據,然后通過類似 notify 等動作,通知消費線程可以繼續工作了。Thread.join() 也會令線程進入等待狀態。
* 計時等待(TIMED\_WAIT),其進入條件和等待狀態類似,但是調用的是存在超時條件的方法,比如 wait 或 join 等方法的指定超時版本,如下面示例:
~~~
public final native void wait(long timeout) throws InterruptedException;
~~~
* 終止(TERMINATED),不管是意外退出還是正常執行結束,線程已經完成使命,終止運行,也有人把這個狀態叫作死亡。
在第二次調用 start() 方法的時候,線程可能處于終止或者其他(非 NEW)狀態,但是不論如何,都是不可以再次啟動的。
## 考點分析
今天的問題可以算是個常見的面試熱身題目,前面的給出的典型回答,算是對基本狀態和簡單流轉的一個介紹,如果覺得還不夠直觀,我在下面分析會對比一個狀態圖進行介紹。總的來說,理解線程對于我們日常開發或者診斷分析,都是不可或缺的基礎。
面試官可能會以此為契機,從各種不同角度考察你對線程的掌握:
* 相對理論一些的面試官可以會問你線程到底是什么以及 Java 底層實現方式。
* 線程狀態的切換,以及和鎖等并發工具類的互動。
* 線程編程時容易踩的坑與建議等。
可以看出,僅僅是一個線程,就有非常多的內容需要掌握。我們選擇重點內容,開始進入詳細分析。
## 知識擴展
首先,我們來整體看一下線程是什么?
從操作系統的角度,可以簡單認為,線程是系統調度的最小單元,一個進程可以包含多個線程,作為任務的真正運作者,有自己的棧(Stack)、寄存器(Register)、本地存儲(Thread Local)等,但是會和進程內其他線程共享文件描述符、虛擬地址空間等。
在具體實現中,線程還分為內核線程、用戶線程,Java 的線程實現其實是與虛擬機相關的。對于我們最熟悉的 Sun/Oracle JDK,其線程也經歷了一個演進過程,基本上在 Java 1.2 之后,JDK 已經拋棄了所謂的[Green Thread](https://en.wikipedia.org/wiki/Green_threads),也就是用戶調度的線程,現在的模型是一對一映射到操作系統內核線程。
如果我們來看 Thread 的源碼,你會發現其基本操作邏輯大都是以 JNI 形式調用的本地代碼。
~~~
private native void start0();
private native void setPriority0(int newPriority);
private native void interrupt0();
~~~
這種實現有利有弊,總體上來說,Java 語言得益于精細粒度的線程和相關的并發操作,其構建高擴展性的大型應用的能力已經毋庸置疑。但是,其復雜性也提高了并發編程的門檻,近幾年的 Go 語言等提供了協程([coroutine](https://en.wikipedia.org/wiki/Coroutine)),大大提高了構建并發應用的效率。于此同時,Java 也在[Loom](http://openjdk.java.net/projects/loom/)項目中,孕育新的類似輕量級用戶線程(Fiber)等機制,也許在不久的將來就可以在新版 JDK 中使用到它。
下面,我來分析下線程的基本操作。如何創建線程想必你已經非常熟悉了,請看下面的例子:
~~~
Runnable task = () -> {System.out.println("Hello World!");};
Thread myThread = new Thread(task);
myThread.start();
myThread.join();
~~~
我們可以直接擴展 Thread 類,然后實例化。但在本例中,我選取了另外一種方式,就是實現一個 Runnable,將代碼邏放在 Runnable 中,然后構建 Thread 并啟動(start),等待結束(join)。
Runnable 的好處是,不會受 Java 不支持類多繼承的限制,重用代碼實現,當我們需要重復執行相應邏輯時優點明顯。而且,也能更好的與現代 Java 并發庫中的 Executor 之類框架結合使用,比如將上面 start 和 join 的邏輯完全寫成下面的結構:
~~~
Future future = Executors.newFixedThreadPool(1)
.submit(task)
.get();
~~~
這樣我們就不用操心線程的創建和管理,也能利用 Future 等機制更好地處理執行結果。線程生命周期通常和業務之間沒有本質聯系,混淆實現需求和業務需求,就會降低開發的效率。
從線程生命周期的狀態開始展開,那么在 Java 編程中,有哪些因素可能影響線程的狀態呢?主要有:
* 線程自身的方法,除了 start,還有多個 join 方法,等待線程結束;yield 是告訴調度器,主動讓出 CPU;另外,就是一些已經被標記為過時的 resume、stop、suspend 之類,據我所知,在 JDK 最新版本中,destory/stop 方法將被直接移除。
* 基類 Object 提供了一些基礎的 wait/notify/notifyAll 方法。如果我們持有某個對象的 Monitor 鎖,調用 wait 會讓當前線程處于等待狀態,直到其他線程 notify 或者 notifyAll。所以,本質上是提供了 Monitor 的獲取和釋放的能力,是基本的線程間通信方式。
* 并發類庫中的工具,比如 CountDownLatch.await() 會讓當前線程進入等待狀態,直到 latch 被基數為 0,這可以看作是線程間通信的 Signal。
我這里畫了一個狀態和方法之間的對應圖:

Thread 和 Object 的方法,聽起來簡單,但是實際應用中被證明非常晦澀、易錯,這也是為什么 Java 后來又引入了并發包。總的來說,有了并發包,大多數情況下,我們已經不再需要去調用 wait/notify 之類的方法了。
前面談了不少理論,下面談談線程 API 使用,我會側重于平時工作學習中,容易被忽略的一些方面。
先來看看守護線程(Daemon Thread),有的時候應用中需要一個長期駐留的服務程序,但是不希望其影響應用退出,就可以將其設置為守護線程,如果 JVM 發現只有守護線程存在時,將結束進程,具體可以參考下面代碼段。**注意,必須在線程啟動之前設置。**
~~~
hread daemonThread = new Thread();
daemonThread.setDaemon(true);
daemonThread.start();
~~~
再來看看[Spurious wakeup](https://en.wikipedia.org/wiki/Spurious_wakeup)。尤其是在多核 CPU 的系統中,線程等待存在一種可能,就是在沒有任何線程廣播或者發出信號的情況下,線程就被喚醒,如果處理不當就可能出現詭異的并發問題,所以我們在等待條件過程中,建議采用下面模式來書寫。
~~~
// 推薦
while ( isCondition()) {
waitForAConfition(...);
}
// 不推薦,可能引入 bug
if ( isCondition()) {
waitForAConfition(...);
}
~~~
Thread.onSpinWait(),這是 Java 9 中引入的特性。我在[專欄第 16 講](http://time.geekbang.org/column/article/9042)給你留的思考題中,提到“自旋鎖”(spin-wait, busy-waiting),也可以認為其不算是一種鎖,而是一種針對短期等待的性能優化技術。“onSpinWait()”沒有任何行為上的保證,而是對 JVM 的一個暗示,JVM 可能會利用 CPU 的 pause 指令進一步提高性能,性能特別敏感的應用可以關注。
再有就是慎用[ThreadLocal](https://docs.oracle.com/javase/9/docs/api/java/lang/ThreadLocal.html),這是 Java 提供的一種保存線程私有信息的機制,因為其在整個線程生命周期內有效,所以可以方便地在一個線程關聯的不同業務模塊之間傳遞信息,比如事務 ID、Cookie 等上下文相關信息。
它的實現結構,可以參考[源碼](http://hg.openjdk.java.net/jdk/jdk/file/ee8524126794/src/java.base/share/classes/java/lang/ThreadLocal.java),數據存儲于線程相關的 ThreadLocalMap,其內部條目是弱引用,如下面片段。
~~~
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
// …
}
~~~
當 Key 為 null 時,該條目就變成“廢棄條目”,相關“value”的回收,往往依賴于幾個關鍵點,即 set、remove、rehash。
下面是 set 的示例,我進行了精簡和注釋:
~~~
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];; …) {
//…
if (k == null) {
// 替換廢棄條目
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
// 掃描并清理發現的廢棄條目,并檢查容量是否超限
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();// 清理廢棄條目,如果仍然超限,則擴容(加倍)
}
~~~
具體的清理邏輯是實現在 cleanSomeSlots 和 expungeStaleEntry 之中,如果你有興趣可以自行閱讀。
結合[專欄第 4 講](http://time.geekbang.org/column/article/6970)介紹的引用類型,我們會發現一個特別的地方,通常弱引用都會和引用隊列配合清理機制使用,但是 ThreadLocal 是個例外,它并沒有這么做。
這意味著,廢棄項目的回收**依賴于顯式地觸發,否則就要等待線程結束**,進而回收相應 ThreadLocalMap!這就是很多 OOM 的來源,所以通常都會建議,應用一定要自己負責 remove,并且不要和線程池配合,因為 worker 線程往往是不會退出的。
今天,我介紹了線程基礎,分析了生命周期中的狀態和各種方法之間的對應關系,這也有助于我們更好地理解 synchronized 和鎖的影響,并介紹了一些需要注意的操作,希望對你有所幫助。
## 一課一練
關于今天我們討論的題目你做到心中有數了嗎?今天我準備了一個有意思的問題,寫一個最簡單的打印 HelloWorld 的程序,說說看,運行這個應用,Java 至少會創建幾個線程呢?然后思考一下,如何明確驗證你的結論,真實情況很可能令你大跌眼鏡哦。
- 前言
- 開篇詞
- 開篇詞 -以面試題為切入點,有效提升你的Java內功
- 模塊一 Java基礎
- 第1講 談談你對Java平臺的理解?
- 第2講 Exception和Error有什么區別?
- 第3講 談談final、finally、 finalize有什么不同?
- 第4講 強引用、軟引用、弱引用、幻象引用有什么區別?
- 第5講 String、StringBuffer、StringBuilder有什么區別?
- 第6講 動態代理是基于什么原理?
- 第7講 int和Integer有什么區別?
- 第8講 對比Vector、ArrayList、LinkedList有何區別?
- 第9講 對比Hashtable、HashMap、TreeMap有什么不同?
- 第10講 如何保證集合是線程安全的? ConcurrentHashMap如何實現高效地線程安全?
- 第11講 Java提供了哪些IO方式? NIO如何實現多路復用?
- 第12講 Java有幾種文件拷貝方式?哪一種最高效?
- 第13講 談談接口和抽象類有什么區別?
- 第14講 談談你知道的設計模式?
- 模塊二 Java進階
- 第15講 synchronized和ReentrantLock有什么區別呢?
- 第16講 synchronized底層如何實現?什么是鎖的升級、降級?
- 第17講 一個線程兩次調用start()方法會出現什么情況?
- 第18講 什么情況下Java程序會產生死鎖?如何定位、修復?
- 第19講 Java并發包提供了哪些并發工具類?
- 第20講 并發包中的ConcurrentLinkedQueue和LinkedBlockingQueue有什么區別?
- 第21講 Java并發類庫提供的線程池有哪幾種? 分別有什么特點?
- 第22講 AtomicInteger底層實現原理是什么?如何在自己的產品代碼中應用CAS操作?
- 第23講 請介紹類加載過程,什么是雙親委派模型?
- 第24講 有哪些方法可以在運行時動態生成一個Java類?
- 第25講 談談JVM內存區域的劃分,哪些區域可能發生OutOfMemoryError?
- 第26講 如何監控和診斷JVM堆內和堆外內存使用?
- 第27講 Java常見的垃圾收集器有哪些?
- 第28講 談談你的GC調優思路?
- 第29講 Java內存模型中的happen-before是什么?
- 第30講 Java程序運行在Docker等容器環境有哪些新問題?
- 模塊三 Java安全基礎
- 第31講 你了解Java應用開發中的注入攻擊嗎?
- 第32講 如何寫出安全的Java代碼?
- 模塊四 Java性能基礎
- 第33講 后臺服務出現明顯“變慢”,談談你的診斷思路?
- 第34講 有人說“Lambda能讓Java程序慢30倍”,你怎么看?
- 第35講 JVM優化Java代碼時都做了什么?
- 模塊五 Java應用開發擴展
- 第36講 談談MySQL支持的事務隔離級別,以及悲觀鎖和樂觀鎖的原理和應用場景?
- 第37講 談談Spring Bean的生命周期和作用域?
- 第38講 對比Java標準NIO類庫,你知道Netty是如何實現更高性能的嗎?
- 第39講 談談常用的分布式ID的設計方案?Snowflake是否受冬令時切換影響?
- 周末福利
- 周末福利 談談我對Java學習和面試的看法
- 周末福利 一份Java工程師必讀書單
- 結束語
- 結束語 技術沒有終點
- 結課測試 Java核心技術的這些知識,你真的掌握了嗎?