摘要:在領(lǐng)域,實現(xiàn)并發(fā)程序的主要手段就是多線程。可運行狀態(tài)指的是線程可以分配執(zhí)行。當(dāng)?shù)却氖录霈F(xiàn)了,線程就會從休眠狀態(tài)轉(zhuǎn)換到可運行狀態(tài)。導(dǎo)出線程棧,分析線程狀態(tài)是診斷并發(fā)問題的一個重要工具。
在 Java 領(lǐng)域,實現(xiàn)并發(fā)程序的主要手段就是多線程。線程是操作系統(tǒng)里的一個概念,雖然各種不同的開發(fā)語言如 Java、C# 等都對其進(jìn)行了封裝,但原理和思路都是相同都。Java 語言里的線程本質(zhì)上就是操作系統(tǒng)的線程,它們是一一對應(yīng)的。
在操作系統(tǒng)層面,線程也有“生老病死”,專業(yè)的說法叫有生命周期。對于有生命周期的事物,要學(xué)好它,只要能搞懂生命周期中各個節(jié)點的狀態(tài)轉(zhuǎn)換機(jī)制就可以了。
雖然不同的開發(fā)語言對于操作系統(tǒng)線程進(jìn)行了不同的封裝,但是對于線程的生命周期這部分,基本上是雷同的。所以,我們可以先來了解一下通用的線程生命周期模型,然后再詳細(xì)的學(xué)習(xí)一下 Java 中線程的生命周期。
通用的線程生命周期通用的線程生命周期基本上可以用下圖這個“五態(tài)模型”來描述。這五態(tài)分別是:
初始狀態(tài)、可運行狀態(tài)、運行狀態(tài)、休眠狀態(tài) 和 終止?fàn)顟B(tài)
通用線程狀態(tài)轉(zhuǎn)換圖——五態(tài)模型
初始狀態(tài):指的是線程已經(jīng)被創(chuàng)建,但是還不允許分配 CPU 執(zhí)行。這個狀態(tài)屬于編程語言特有的,不過這里所謂的被創(chuàng)建,僅僅是在編程語言層面被創(chuàng)建,而在操作系統(tǒng)層面,真正的線程還沒有創(chuàng)建。
可運行狀態(tài):指的是線程可以分配 CPU 執(zhí)行。在這種狀態(tài)下,真正的操作系統(tǒng)線程已經(jīng)被成功創(chuàng)建了,所以可以分配 CPU 執(zhí)行。
運行狀態(tài):當(dāng)有空閑的 CPU 時,操作系統(tǒng)會將其分配給一個處于可運行狀態(tài)的線程,被分配到 CPU 的線程的狀態(tài)就轉(zhuǎn)換成了運行狀態(tài)
休眠狀態(tài):運行狀態(tài)的線程如果調(diào)用一個阻塞的 API(例如以阻塞方式讀文件)或者等待某個事件(例如條件變量),那么線程的狀態(tài)就會轉(zhuǎn)換到 休眠狀態(tài),同時釋放 CPU 使用權(quán),休眠狀態(tài)的線程永遠(yuǎn)沒有機(jī)會獲得 CPU 使用權(quán)。當(dāng)?shù)却氖录霈F(xiàn)了,線程就會從休眠狀態(tài)轉(zhuǎn)換到可運行狀態(tài)。
終止?fàn)顟B(tài):線程執(zhí)行完或者出現(xiàn)異常就會進(jìn)入 終止?fàn)顟B(tài),終止?fàn)顟B(tài)的線程不會切換到其他任何狀態(tài),進(jìn)入終止?fàn)顟B(tài)也就意味著線程的生命周期結(jié)束了。
這五種狀態(tài)在不同編程語言里會有簡化合并或者被細(xì)化。Java 中線程的生命周期
Java 語言中線程共有六種狀態(tài),分別是:
NEW(初始化狀態(tài))
RUNNABLE(可運行 / 運行狀態(tài))
BLOCKED(阻塞狀態(tài))
WAITING(無時限等待)
TIMED_WAITING(有時限等待)
TERMINATED(終止?fàn)顟B(tài))
在操作系統(tǒng)層面,Java 線程中的 BLOCKED、WAITING、TIMED_WAITING 是一種狀態(tài),即前面我們提到的休眠狀態(tài)。也就是說
只要 Java 線程處于這三種狀態(tài)之一,那么這個線程就永遠(yuǎn)沒有 CPU 的使用權(quán)。
所以 Java 線程的生命周期可以簡化為下圖:
Java 中的線程狀態(tài)轉(zhuǎn)換圖
其中,BLOCKED、WAITING、TIMED_WAITING 可以理解為線程導(dǎo)致休眠狀態(tài)的三種原因。那具體是哪些情形會導(dǎo)致線程從 RUNNABLE 狀態(tài)轉(zhuǎn)換到這三種狀態(tài)呢?而這三種狀態(tài)又是何時轉(zhuǎn)換回 RUNNABLE 的呢?以及 NEW、TERMINATED 和 RUNNABLE 狀態(tài)是如何轉(zhuǎn)換的?
1. RUNNABLE 與 BLOCKED 的狀態(tài)轉(zhuǎn)換只有一種場景會觸發(fā)這種轉(zhuǎn)換,就是線程等待 synchronized 的隱式鎖。synchronized 修飾的方法、代碼塊同一時刻只允許一個線程執(zhí)行,其他線程只能等待,這種情況下,等待的線程就會從 RUNNABLE 轉(zhuǎn)換到 BLOCKED 狀態(tài)。而當(dāng)?shù)却木€程獲得 synchronized 隱式鎖時,就又會從 BLOCKED 轉(zhuǎn)換到 RUNNABLE 狀態(tài)。
如果你熟悉操作系統(tǒng)線程的生命周期的話,可能會有個疑問:線程調(diào)用阻塞式 API 時,是否會轉(zhuǎn)換到 BLOCKED 狀態(tài)呢?在操作系統(tǒng)層面,線程是會轉(zhuǎn)換到休眠狀態(tài)的,但是在 JVM 層面,Java 線程的狀態(tài)不會發(fā)生變化,也就是說 Java 線程的狀態(tài)會依然保持 RUNNABLE 狀態(tài)。
JVM 層面并不關(guān)心操作系統(tǒng)調(diào)度相關(guān)的狀態(tài) ,因為在 JVM 看來,等待 CPU 使用權(quán)(操作系統(tǒng)層面此時處于可執(zhí)行狀態(tài))與等待 I/O(操作系統(tǒng)層面此時處于休眠狀態(tài))沒有區(qū)別,都是在等待某個資源,所以都?xì)w入了 RUNNABLE 狀態(tài)。
而我們平時所謂的 Java 在調(diào)用阻塞式 API 時,線程會阻塞,指的是操作系統(tǒng)線程的狀態(tài),并不是 Java 線程的狀態(tài)。
2. RUNNABLE 與 WAITING 的狀態(tài)轉(zhuǎn)換有三種場景會觸發(fā)這種轉(zhuǎn)換。
第一種場景,獲得 synchronized 隱式鎖的線程,調(diào)用無參數(shù)的 Object.wait() 方法。
第二種場景,調(diào)用無參數(shù)的 Thread.join() 方法。其中的 join() 是一種線程同步方法,例如有一個線程對象 thread A,當(dāng)調(diào)用 A.join() 的時候,執(zhí)行這條語句的線程會等待 thread A 執(zhí)行完,而等待中的這個線程,其狀態(tài)會從 RUNNABLE 轉(zhuǎn)換到 WAITING。當(dāng)線程 thread A 執(zhí)行完,原來等待它的線程又會從 WAITING 狀態(tài)轉(zhuǎn)換到 RUNNABLE。
第三種場景,調(diào)用 LockSupport.park() 方法。其中的 LockSupport 對象,也許你有點陌生,其實 Java 并發(fā)包中的鎖,都是基于它實現(xiàn)的。調(diào)用 LockSupport.park() 方法,當(dāng)前線程會阻塞,線程的狀態(tài)會從 RUNNABLE 轉(zhuǎn)換到 WAITING。調(diào)用 LockSupport.unpark(Thread thread) 可喚醒目標(biāo)線程,目標(biāo)線程的狀態(tài)又會從 WAITING 狀態(tài)轉(zhuǎn)換到 RUNNABLE。
3. RUNNABLE 與 TIMED_WAITING 的狀態(tài)轉(zhuǎn)換有五種場景會觸發(fā)這種轉(zhuǎn)換:
調(diào)用帶超時參數(shù)的 Thread.sleep(long millis) 方法;
獲得 synchronized 隱式鎖的線程,調(diào)用帶超時參數(shù)的 Object.wait(long timeout) 方法;
調(diào)用帶超時參數(shù)的 Thread.join(long millis) 方法;
調(diào)用帶超時參數(shù)的 LockSupport.parkNanos(Object blocker, long deadline) 方法;
調(diào)用帶超時參數(shù)的 LockSupport.parkUntil(long deadline) 方法。
TIMED_WAITING 和 WAITING 狀態(tài)的區(qū)別,僅僅是觸發(fā)條件多了 超時參數(shù)。
4. 從 NEW 到 RUNNABLE 狀態(tài)Java 剛創(chuàng)建出來的 Thread 對象就是 NEW 狀態(tài),而創(chuàng)建 Thread 對象主要有兩種方法。一種是繼承 Thread 對象,重寫 run() 方法。示例代碼如下:
// 自定義線程對象 class MyThread extends Thread { public void run() { // 線程需要執(zhí)行的代碼 ...... } } // 創(chuàng)建線程對象 MyThread myThread = new MyThread();
另一種是實現(xiàn) Runnable 接口,重寫 run() 方法,并將該實現(xiàn)類作為創(chuàng)建 Thread 對象的參數(shù)。示例代碼如下:
// 實現(xiàn) Runnable 接口 class Runner implements Runnable { @Override public void run() { // 線程需要執(zhí)行的代碼 ...... } } // 創(chuàng)建線程對象 Thread thread = new Thread(new Runner());
NEW 狀態(tài)的線程,不會被操作系統(tǒng)調(diào)度,因此不會執(zhí)行。Java 線程要執(zhí)行,就必須轉(zhuǎn)換到 RUNNABLE 狀態(tài)。從 NEW 狀態(tài)轉(zhuǎn)換到 RUNNABLE 狀態(tài)很簡單,只要調(diào)用線程對象的 start() 方法就可以了,示例代碼如下:
MyThread myThread = new MyThread(); // 從 NEW 狀態(tài)轉(zhuǎn)換到 RUNNABLE 狀態(tài) myThread.start();5. 從 RUNNABLE 到 TERMINATED 狀態(tài)
線程執(zhí)行完 run() 方法后,會自動轉(zhuǎn)換到 TERMINATED 狀態(tài),當(dāng)然如果執(zhí)行 run() 方法的時候異常拋出,也會導(dǎo)致線程終止。有時候我們需要強(qiáng)制中斷 run() 方法的執(zhí)行,例如 run() 方法訪問一個很慢的網(wǎng)絡(luò),我們等不下去了,想終止怎么辦呢?Java 的 Thread 類里面倒是有個 stop() 方法,不過已經(jīng)標(biāo)記為 @Deprecated,所以不建議使用了。正確的姿勢其實是調(diào)用 interrupt() 方法。
stop() 和 interrupt() 方法的主要區(qū)別?stop() 方法會真的殺死線程,不給線程喘息的機(jī)會,如果線程持有 ReentrantLock 鎖,被 stop() 的線程并不會自動調(diào)用 ReentrantLock 的 unlock() 去釋放鎖,那其他線程就再也沒機(jī)會獲得 ReentrantLock 鎖。所以該方法就不建議使用了,類似的方法還有 suspend() 和 resume() 方法,這兩個方法同樣也都不建議使用。
而 interrupt() 方法僅僅是通知線程,線程有機(jī)會執(zhí)行一些后續(xù)操作,同時也可以無視這個通知。被 interrupt 的線程,是怎么收到通知的呢?一種是異常,另一種是主動檢測。
當(dāng)線程 A 處于 WAITING、TIMED_WAITING 狀態(tài)時,如果其他線程調(diào)用線程 A 的 interrupt() 方法,會使線程 A 返回到 RUNNABLE 狀態(tài),同時線程 A 的代碼會觸發(fā) InterruptedException 異常。上面我們提到轉(zhuǎn)換到 WAITING、TIMED_WAITING 狀態(tài)的觸發(fā)條件,都是調(diào)用了類似 wait()、join()、sleep() 這樣的方法,我們看這些方法的簽名,發(fā)現(xiàn)都會 throws InterruptedException 這個異常。這個異常的觸發(fā)條件就是:其他線程調(diào)用了該線程的 interrupt() 方法。
當(dāng)線程 A 處于 RUNNABLE 狀態(tài)時,并且阻塞在 java.nio.channels.InterruptibleChannel 上時,如果其他線程調(diào)用線程 A 的 interrupt() 方法,線程 A 會觸發(fā) java.nio.channels.ClosedByInterruptException 這個異常;而阻塞在 java.nio.channels.Selector 上時,如果其他線程調(diào)用線程 A 的 interrupt() 方法,線程 A 的 java.nio.channels.Selector 會立即返回。
還有一種是主動檢測,如果線程處于 RUNNABLE 狀態(tài),并且沒有阻塞在某個 I/O 操作上,例如中斷計算圓周率的線程 A,這時就得依賴線程 A 主動檢測中斷狀態(tài)了。如果其他線程調(diào)用線程 A 的 interrupt() 方法,那么線程 A 可以通過 isInterrupted() 方法,檢測是不是自己被中斷了。
Java線程的生命周期小結(jié)多線程程序很難調(diào)試,出了 Bug 基本上都是靠日志,靠線程 dump 來跟蹤問題,分析線程 dump 的一個基本功就是分析線程狀態(tài),大部分的死鎖、饑餓、活鎖問題都需要跟蹤分析線程的狀態(tài)。
通過 jstack 命令或者 Java VisualVM 這個可視化工具將 JVM 所有的線程棧信息導(dǎo)出來,完整的線程棧信息不僅包括線程的當(dāng)前狀態(tài)、調(diào)用棧,還包括了鎖的信息。導(dǎo)出線程棧,分析線程狀態(tài)是診斷并發(fā)問題的一個重要工具。
創(chuàng)建多少線程才是合適的?在 Java 領(lǐng)域,實現(xiàn)并發(fā)程序的主要手段就是多線程,使用多線程還是比較簡單的,但是使用多少個線程卻是個困難的問題。工作中,經(jīng)常有人問,“各種線程池的線程數(shù)量調(diào)整成多少是合適的?
要解決這個問題,首先要分析以下兩個問題:
為什么要使用多線程?
多線程的應(yīng)用場景有哪些?
為什么使用多線程使用多線程,本質(zhì)上就是提升程序性能。不過此刻談到的性能,首要問題是:如何度量性能。
度量性能的指標(biāo)有很多,但是有兩個指標(biāo)是最核心的,它們就是延遲和吞吐量。
延遲指的是發(fā)出請求到收到響應(yīng)這個過程的時間;延遲越短,意味著程序執(zhí)行得越快,性能也就越好。
吞吐量指的是在單位時間內(nèi)能處理請求的數(shù)量;吞吐量越大,意味著程序能處理的請求越多,性能也就越好。這兩個指標(biāo)內(nèi)部有一定的聯(lián)系(同等條件下,延遲越短,吞吐量越大),但是由于它們隸屬不同的維度(一個是時間維度,一個是空間維度),并不能互相轉(zhuǎn)換。
我們所謂提升性能,從度量的角度,主要是降低延遲,提高吞吐量。這也是我們使用多線程的主要目的。那我們該怎么降低延遲,提高吞吐量呢?這個就要從多線程的應(yīng)用場景說起了。
多線程的應(yīng)用場景要想“降低延遲,提高吞吐量”,對應(yīng)的方法呢,基本上有兩個方向,一個方向是優(yōu)化算法,另一個方向是 將硬件的性能發(fā)揮到極致。前者屬于算法范疇,后者則是和并發(fā)編程相關(guān)了。其實計算機(jī)主要有主要是兩類:一個是 I/O,一個是 CPU。簡言之,在并發(fā)編程領(lǐng)域,提升性能本質(zhì)上就是提升 I/O 的利用率和 CPU 的利用率。多帶帶來看,操作系統(tǒng)已經(jīng)為我們做了利用率的優(yōu)化了,但是解決的是針對單一的硬件利用率。我們的程序執(zhí)行中是既要CPU也要I/O的。所以對于我們開發(fā)者,我們最終需要解決 CPU 和 I/O 設(shè)備綜合利用率的問題。
下面我們用一個簡單的示例來說明:如何利用多線程來提升 CPU 和 I/O 設(shè)備的利用率?假設(shè)程序按照 CPU 計算和 I/O 操作交叉執(zhí)行的方式運行,而且 CPU 計算和 I/O 操作的耗時是 1:1。
如下圖所示,如果只有一個線程,執(zhí)行 CPU 計算的時候,I/O 設(shè)備空閑;執(zhí)行 I/O 操作的時候,CPU 空閑,所以 CPU 的利用率和 I/O 設(shè)備的利用率都是 50%。
如果有兩個線程,如下圖所示,當(dāng)線程 A 執(zhí)行 CPU 計算的時候,線程 B 執(zhí)行 I/O 操作;當(dāng)線程 A 執(zhí)行 I/O 操作的時候,線程 B 執(zhí)行 CPU 計算,這樣 CPU 的利用率和 I/O 設(shè)備的利用率就都達(dá)到了 100%。
通過上面的圖示,很容易看出:單位時間處理的請求數(shù)量翻了一番,也就是說吞吐量提高了 1 倍。此時可以逆向思維一下,如果 CPU 和 I/O 設(shè)備的利用率都很低,那么可以嘗試通過增加線程來提高吞吐量.
創(chuàng)建多少線程合適?創(chuàng)建多少線程合適,要看多線程具體的應(yīng)用場景。我們的程序一般都是 CPU 計算和 I/O 操作交叉執(zhí)行的,由于 I/O 設(shè)備的速度相對于 CPU 來說都很慢,所以大部分情況下,I/O 操作執(zhí)行的時間相對于 CPU 計算來說都非常長,這種場景我們一般都稱為 I/O 密集型計算;和 I/O 密集型計算相對的就是 CPU 密集型計算了,CPU 密集型計算大部分場景下都是純 CPU 計算。I/O 密集型程序和 CPU 密集型程序,計算最佳線程數(shù)的方法是不同的。
對于 CPU 密集型計算,多線程本質(zhì)上是提升多核 CPU 的利用率,所以對于一個 4 核的 CPU,每個核一個線程,理論上創(chuàng)建 4 個線程就可以了,再多創(chuàng)建線程也只是增加線程切換的成本。所以,
對于 CPU 密集型的計算場景,理論上“線程的數(shù)量 =CPU 核數(shù)”就是最合適的。不過在工程上,線程的數(shù)量一般會設(shè)置為“CPU 核數(shù) +1”
因為當(dāng)線程因為偶爾的內(nèi)存頁失效或其他原因?qū)е伦枞麜r,這個額外的線程可以頂上,從而保證 CPU 的利用率。
對于 I/O 密集型的計算場景,比如前面我們的例子中,如果 CPU 計算和 I/O 操作的耗時是 1:1,那么 2 個線程是最合適的。如果 CPU 計算和 I/O 操作的耗時是 1:2,那多少個線程合適呢?是 3 個線程,如下圖所示:CPU 在 A、B、C 三個線程之間切換,對于線程 A,當(dāng) CPU 從 B、C 切換回來時,線程 A 正好執(zhí)行完 I/O 操作。這樣 CPU 和 I/O 設(shè)備的利用率都達(dá)到了 100%。
三線程執(zhí)行示意圖
通過上面這個例子,我們會發(fā)現(xiàn),對于 I/O 密集型計算場景,最佳的線程數(shù)是與程序中 CPU 計算和 I/O 操作的耗時比相關(guān)的,我們可以總結(jié)出這樣一個公式:
最佳線程數(shù) =1 +(I/O 耗時 / CPU 耗時)
我們令 R=I/O 耗時 / CPU 耗時,綜合上圖,可以這樣理解:當(dāng)線程 A 執(zhí)行 IO 操作時,另外 R 個線程正好執(zhí)行完各自的 CPU 計算。這樣 CPU 的利用率就達(dá)到了 100%。
多核 CPU,只需要等比擴(kuò)大就可以了,計算公式如下:
最佳線程數(shù) =CPU 核數(shù) * [ 1 +(I/O 耗時 / CPU 耗時)]線程多少的總結(jié)
對于 I/O 密集型計算場景,I/O 耗時和 CPU 耗時的比值是一個關(guān)鍵參數(shù),不幸的是這個參數(shù)是未知的,而且是動態(tài)變化的,所以工程上,我們要估算這個參數(shù),然后做各種不同場景下的壓測來驗證我們的估計。所以壓測時,我們需要重點關(guān)注 CPU、I/O 設(shè)備的利用率和性能指標(biāo)(響應(yīng)時間、吞吐量)之間的關(guān)系。
耗時的比值需要使用APM工具觀察得出。
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://specialneedsforspecialkids.com/yun/77628.html
摘要:假設(shè)不發(fā)生編譯器重排和指令重排,線程修改了的值,但是修改以后,的值可能還沒有寫回到主存中,那么線程得到就是很自然的事了。同理,線程對于的賦值操作也可能沒有及時刷新到主存中。線程的最后操作與線程發(fā)現(xiàn)線程已經(jīng)結(jié)束同步。 很久沒更新文章了,對隔三差五過來刷更新的讀者說聲抱歉。 關(guān)于 Java 并發(fā)也算是寫了好幾篇文章了,本文將介紹一些比較基礎(chǔ)的內(nèi)容,注意,閱讀本文需要一定的并發(fā)基礎(chǔ)。 本文的...
執(zhí)行器 在前面的所有示例中,由新的線程(由其Runnable對象定義)和線程本身(由Thread對象定義)完成的任務(wù)之間存在緊密的聯(lián)系,這適用于小型應(yīng)用程序,但在大型應(yīng)用程序中,將線程管理和創(chuàng)建與應(yīng)用程序的其余部分分開是有意義的,封裝這些函數(shù)的對象稱為執(zhí)行器,以下小節(jié)詳細(xì)描述了執(zhí)行器。 執(zhí)行器接口定義三個執(zhí)行器對象類型。 線程池是最常見的執(zhí)行器實現(xiàn)類型。 Fork/Join是一個利用多個處理器的...
摘要:虛擬機(jī)棧線程私有,生命周期跟線程相同。堆用于存放對象實例,是虛擬機(jī)所管理的內(nèi)存中最大的一塊,同時也是所有線程共享的一塊內(nèi)存區(qū)域。統(tǒng)計監(jiān)測工具語法格式如下是虛擬機(jī),在系統(tǒng)上一般就是進(jìn)程。 JDK、JRE、JVM三者的關(guān)系 JDK(Java Development Kit)是針對Java開發(fā)的產(chǎn)品、是整個Java的核心,包括Java運行環(huán)境JRE、Java工具包和Java基礎(chǔ)類庫。 JR...
摘要:本文是成為專家系列的第一篇。然而,在多線程環(huán)境下,將會有別樣的狀況。在中正是通過解決了多線程問題。在最后的并發(fā)清理階段,垃圾回收過程被真正執(zhí)行。在垃圾回收執(zhí)行過程中,其他線程依然在執(zhí)行。 原文鏈接:http://www.cubrid.org/blog/de... 了解Java的垃圾回收(GC)原理能給我們帶來什么好處?對于軟件工程師來說,滿足技術(shù)好奇心可算是一個,但重要的是理解GC能幫...
閱讀 2866·2021-10-08 10:12
閱讀 3966·2021-09-22 15:45
閱讀 2555·2019-08-30 15:52
閱讀 2625·2019-08-29 18:44
閱讀 2644·2019-08-29 12:37
閱讀 1154·2019-08-26 13:36
閱讀 2561·2019-08-26 13:34
閱讀 1473·2019-08-26 12:20