在 Java 領域,實現併發程序的主要手段就是多線程。線程是操做系統裏的一個概念,雖然各類不一樣的開發語言如 Java、C# 等都對其進行了封裝,但原理和思路都是相同都。Java 語言裏的線程本質上就是操做系統的線程,它們是一一對應的。java
在操做系統層面,線程也有「生老病死」,專業的說法叫有生命週期。對於有生命週期的事物,要學好它,只要能搞懂生命週期中各個節點的狀態轉換機制
就能夠了。算法
雖然不一樣的開發語言對於操做系統線程進行了不一樣的封裝,可是對於線程的生命週期這部分,基本上是雷同的。因此,咱們能夠先來了解一下通用的線程生命週期模型,而後再詳細的學習一下 Java 中線程的生命週期。編程
通用的線程生命週期基本上能夠用下圖這個「五態模型」來描述。這五態分別是:網絡
初始狀態、可運行狀態、運行狀態、休眠狀態 和 終止狀態
通用線程狀態轉換圖——五態模型多線程
初始狀態
:指的是線程已經被建立,可是還不容許分配 CPU 執行。這個狀態屬於編程語言特有的,不過這裏所謂的被建立,僅僅是在編程語言層面被建立,而在操做系統層面,真正的線程尚未建立。可運行狀態
:指的是線程能夠分配 CPU 執行。在這種狀態下,真正的操做系統線程已經被成功建立了,因此能夠分配 CPU 執行。運行狀態
:當有空閒的 CPU 時,操做系統會將其分配給一個處於可運行狀態的線程,被分配到 CPU 的線程的狀態就轉換成了運行狀態休眠狀態
,同時釋放 CPU 使用權,休眠狀態的線程永遠沒有機會得到 CPU 使用權。當等待的事件出現了,線程就會從休眠狀態轉換到可運行狀態。終止狀態
,終止狀態的線程不會切換到其餘任何狀態,進入終止狀態也就意味着線程的生命週期結束了。這五種狀態在不一樣編程語言裏會有簡化合並或者被細化。
Java 語言中線程共有六種狀態,分別是:併發
在操做系統層面,Java 線程中的 BLOCKED、WAITING、TIMED_WAITING 是一種狀態,即前面咱們提到的休眠狀態。也就是說編程語言
只要 Java 線程處於這三種狀態之一,那麼這個線程就永遠沒有 CPU 的使用權。
因此 Java 線程的生命週期能夠簡化爲下圖:ide
Java 中的線程狀態轉換圖工具
其中,BLOCKED、WAITING、TIMED_WAITING 能夠理解爲線程致使休眠狀態的三種緣由。那具體是哪些情形會致使線程從 RUNNABLE 狀態轉換到這三種狀態呢?而這三種狀態又是什麼時候轉換回 RUNNABLE 的呢?以及 NEW、TERMINATED 和 RUNNABLE 狀態是如何轉換的?性能
只有一種場景會觸發這種轉換,就是線程等待 synchronized 的隱式鎖。synchronized 修飾的方法、代碼塊同一時刻只容許一個線程執行,其餘線程只能等待,這種狀況下,等待的線程就會從 RUNNABLE 轉換到 BLOCKED 狀態。而當等待的線程得到 synchronized 隱式鎖時,就又會從 BLOCKED 轉換到 RUNNABLE 狀態。
若是你熟悉操做系統線程的生命週期的話,可能會有個疑問:線程調用阻塞式 API 時,是否會轉換到 BLOCKED 狀態呢?在操做系統層面,線程是會轉換到休眠狀態的,可是在 JVM 層面,Java 線程的狀態不會發生變化,也就是說 Java 線程的狀態會依然保持 RUNNABLE 狀態。
JVM 層面並不關心操做系統調度相關的狀態 ,由於在 JVM 看來,等待 CPU 使用權(操做系統層面此時處於可執行狀態)與等待 I/O(操做系統層面此時處於休眠狀態)沒有區別,都是在等待某個資源,因此都納入了 RUNNABLE 狀態。
而咱們平時所謂的 Java 在調用阻塞式 API 時,線程會阻塞,指的是操做系統線程的狀態,並非 Java 線程的狀態。
有三種場景會觸發這種轉換。
第一種場景,得到 synchronized 隱式鎖的線程,調用無參數的 Object.wait() 方法。
第二種場景,調用無參數的 Thread.join() 方法。其中的 join() 是一種線程同步方法,例若有一個線程對象 thread A,當調用 A.join() 的時候,執行這條語句的線程會等待 thread A 執行完,而等待中的這個線程,其狀態會從 RUNNABLE 轉換到 WAITING。當線程 thread A 執行完,原來等待它的線程又會從 WAITING 狀態轉換到 RUNNABLE。
第三種場景,調用 LockSupport.park() 方法。其中的 LockSupport 對象,也許你有點陌生,其實 Java 併發包中的鎖,都是基於它實現的。調用 LockSupport.park() 方法,當前線程會阻塞,線程的狀態會從 RUNNABLE 轉換到 WAITING。調用 LockSupport.unpark(Thread thread) 可喚醒目標線程,目標線程的狀態又會從 WAITING 狀態轉換到 RUNNABLE。
有五種場景會觸發這種轉換:
TIMED_WAITING 和 WAITING 狀態的區別,僅僅是觸發條件多了 超時參數。
Java 剛建立出來的 Thread 對象就是 NEW 狀態,而建立 Thread 對象主要有兩種方法。一種是繼承 Thread 對象,重寫 run() 方法。示例代碼以下:
// 自定義線程對象 class MyThread extends Thread { public void run() { // 線程須要執行的代碼 ...... } } // 建立線程對象 MyThread myThread = new MyThread();
另外一種是實現 Runnable 接口,重寫 run() 方法,並將該實現類做爲建立 Thread 對象的參數。示例代碼以下:
// 實現 Runnable 接口 class Runner implements Runnable { @Override public void run() { // 線程須要執行的代碼 ...... } } // 建立線程對象 Thread thread = new Thread(new Runner());
NEW 狀態的線程,不會被操做系統調度,所以不會執行。Java 線程要執行,就必須轉換到 RUNNABLE 狀態。從 NEW 狀態轉換到 RUNNABLE 狀態很簡單,只要調用線程對象的 start() 方法就能夠了,示例代碼以下:
MyThread myThread = new MyThread(); // 從 NEW 狀態轉換到 RUNNABLE 狀態 myThread.start();
線程執行完 run() 方法後,會自動轉換到 TERMINATED 狀態,固然若是執行 run() 方法的時候異常拋出,也會致使線程終止。有時候咱們須要強制中斷 run() 方法的執行,例如 run() 方法訪問一個很慢的網絡,咱們等不下去了,想終止怎麼辦呢?Java 的 Thread 類裏面卻是有個 stop() 方法,不過已經標記爲 @Deprecated,因此不建議使用了。正確的姿式實際上是調用 interrupt() 方法。
stop() 方法會真的殺死線程,不給線程喘息的機會,若是線程持有 ReentrantLock 鎖,被 stop() 的線程並不會自動調用 ReentrantLock 的 unlock() 去釋放鎖,那其餘線程就再也沒機會得到 ReentrantLock 鎖。因此該方法就不建議使用了,相似的方法還有 suspend() 和 resume() 方法,這兩個方法一樣也都不建議使用。
而 interrupt() 方法僅僅是通知線程,線程有機會執行一些後續操做,同時也能夠無視這個通知。被 interrupt 的線程,是怎麼收到通知的呢?一種是異常,另外一種是主動檢測。
當線程 A 處於 WAITING、TIMED_WAITING 狀態時,若是其餘線程調用線程 A 的 interrupt() 方法,會使線程 A 返回到 RUNNABLE 狀態,同時線程 A 的代碼會觸發 InterruptedException 異常。上面咱們提到轉換到 WAITING、TIMED_WAITING 狀態的觸發條件,都是調用了相似 wait()、join()、sleep() 這樣的方法,咱們看這些方法的簽名,發現都會 throws InterruptedException 這個異常。這個異常的觸發條件就是:其餘線程調用了該線程的 interrupt() 方法。
當線程 A 處於 RUNNABLE 狀態時,而且阻塞在 java.nio.channels.InterruptibleChannel 上時,若是其餘線程調用線程 A 的 interrupt() 方法,線程 A 會觸發 java.nio.channels.ClosedByInterruptException 這個異常;而阻塞在 java.nio.channels.Selector 上時,若是其餘線程調用線程 A 的 interrupt() 方法,線程 A 的 java.nio.channels.Selector 會當即返回。
還有一種是主動檢測,若是線程處於 RUNNABLE 狀態,而且沒有阻塞在某個 I/O 操做上,例如中斷計算圓周率的線程 A,這時就得依賴線程 A 主動檢測中斷狀態了。若是其餘線程調用線程 A 的 interrupt() 方法,那麼線程 A 能夠經過 isInterrupted() 方法,檢測是否是本身被中斷了。
多線程程序很難調試,出了 Bug 基本上都是靠日誌,靠線程 dump 來跟蹤問題,分析線程 dump 的一個基本功就是分析線程狀態,大部分的死鎖、飢餓、活鎖問題都須要跟蹤分析線程的狀態。
經過 jstack
命令或者 Java VisualVM
這個可視化工具將 JVM 全部的線程棧信息導出來,完整的線程棧信息不只包括線程的當前狀態、調用棧,還包括了鎖的信息。導出線程棧,分析線程狀態是診斷併發問題的一個重要工具。
在 Java 領域,實現併發程序的主要手段就是多線程,使用多線程仍是比較簡單的,可是使用多少個線程倒是個困難的問題。工做中,常常有人問,「各類線程池的線程數量調整成多少是合適的?
要解決這個問題,首先要分析如下兩個問題:
使用多線程,本質上就是提高程序性能。不過此刻談到的性能,首要問題是:如何度量性能。
度量性能的指標有不少,可是有兩個指標是最核心的,它們就是延遲和吞吐量。延遲
指的是發出請求到收到響應這個過程的時間;延遲越短,意味着程序執行得越快,性能也就越好。 吞吐量
指的是在單位時間內能處理請求的數量;吞吐量越大,意味着程序能處理的請求越多,性能也就越好。這兩個指標內部有必定的聯繫(同等條件下,延遲越短,吞吐量越大),可是因爲它們隸屬不一樣的維度(一個是時間維度,一個是空間維度),並不能互相轉換。
咱們所謂提高性能,從度量的角度,主要是下降延遲,提升吞吐量
。這也是咱們使用多線程的主要目的。那咱們該怎麼下降延遲,提升吞吐量呢?這個就要從多線程的應用場景提及了。
要想「下降延遲,提升吞吐量」,對應的方法呢,基本上有兩個方向,一個方向是優化算法
,另外一個方向是 將硬件的性能發揮到極致
。前者屬於算法範疇,後者則是和併發編程相關了。其實計算機主要有主要是兩類:一個是 I/O,一個是 CPU。簡言之,在併發編程領域,提高性能本質上就是提高 I/O 的利用率和 CPU 的利用率。單獨來看,操做系統已經爲咱們作了利用率的優化了,可是解決的是針對單一的硬件利用率。咱們的程序執行中是既要CPU也要I/O的。因此對於咱們開發者,咱們最終須要解決 CPU 和 I/O 設備綜合利用率的問題。
下面咱們用一個簡單的示例來講明:如何利用多線程來提高 CPU 和 I/O 設備的利用率?假設程序按照 CPU 計算和 I/O 操做交叉執行的方式運行,並且 CPU 計算和 I/O 操做的耗時是 1:1。
以下圖所示,若是隻有一個線程,執行 CPU 計算的時候,I/O 設備空閒;執行 I/O 操做的時候,CPU 空閒,因此 CPU 的利用率和 I/O 設備的利用率都是 50%。
若是有兩個線程,以下圖所示,當線程 A 執行 CPU 計算的時候,線程 B 執行 I/O 操做;當線程 A 執行 I/O 操做的時候,線程 B 執行 CPU 計算,這樣 CPU 的利用率和 I/O 設備的利用率就都達到了 100%。
經過上面的圖示,很容易看出:單位時間處理的請求數量翻了一番,也就是說吞吐量提升了 1 倍。此時能夠逆向思惟一下,若是 CPU 和 I/O 設備的利用率都很低,那麼能夠嘗試經過增長線程來提升吞吐量
.
建立多少線程合適,要看多線程具體的應用場景。咱們的程序通常都是 CPU 計算和 I/O 操做交叉執行的,因爲 I/O 設備的速度相對於 CPU 來講都很慢,因此大部分狀況下,I/O 操做執行的時間相對於 CPU 計算來講都很是長,這種場景咱們通常都稱爲 I/O 密集型計算;和 I/O 密集型計算相對的就是 CPU 密集型計算了,CPU 密集型計算大部分場景下都是純 CPU 計算。I/O 密集型程序和 CPU 密集型程序,計算最佳線程數的方法是不一樣的。
對於 CPU 密集型計算,多線程本質上是提高多核 CPU 的利用率,因此對於一個 4 核的 CPU,每一個核一個線程,理論上建立 4 個線程就能夠了,再多建立線程也只是增長線程切換的成本。因此,
對於 CPU 密集型的計算場景,理論上「線程的數量 =CPU 核數」就是最合適的。不過在工程上,線程的數量通常會設置爲「CPU 核數 +1」
由於當線程由於偶爾的內存頁失效或其餘緣由致使阻塞時,這個額外的線程能夠頂上,從而保證 CPU 的利用率。
對於 I/O 密集型的計算場景,好比前面咱們的例子中,若是 CPU 計算和 I/O 操做的耗時是 1:1,那麼 2 個線程是最合適的。若是 CPU 計算和 I/O 操做的耗時是 1:2,那多少個線程合適呢?是 3 個線程,以下圖所示:CPU 在 A、B、C 三個線程之間切換,對於線程 A,當 CPU 從 B、C 切換回來時,線程 A 正好執行完 I/O 操做。這樣 CPU 和 I/O 設備的利用率都達到了 100%。
三線程執行示意圖
經過上面這個例子,咱們會發現,對於 I/O 密集型計算場景,最佳的線程數是與程序中 CPU 計算和 I/O 操做的耗時比相關的,咱們能夠總結出這樣一個公式:
最佳線程數 =1 +(I/O 耗時 / CPU 耗時)
咱們令 R=I/O 耗時 / CPU 耗時,綜合上圖,能夠這樣理解:當線程 A 執行 IO 操做時,另外 R 個線程正好執行完各自的 CPU 計算。這樣 CPU 的利用率就達到了 100%。
多核 CPU,只須要等比擴大就能夠了,計算公式以下:
最佳線程數 =CPU 核數 * [ 1 +(I/O 耗時 / CPU 耗時)]
對於 I/O 密集型計算場景,I/O 耗時和 CPU 耗時的比值是一個關鍵參數,不幸的是這個參數是未知的,並且是動態變化的,因此工程上,咱們要估算這個參數,而後作各類不一樣場景下的壓測來驗證咱們的估計。因此壓測時,咱們須要重點關注 CPU、I/O 設備的利用率和性能指標(響應時間、吞吐量)之間的關係。耗時的比值須要使用APM工具觀察得出。