介紹執行緒
執行緒是系統調度的最小單元,一個行程可以包含多個執行緒,執行緒是負責執行二進制指令的,
每個執行緒有自己的程式計數器、堆疊(Stack)、暫存器(Register)、本地存盤(Thread Local)等,但是會和行程內其他執行緒共享檔案描述符、虛擬地址空間等,
對于任何一個行程來講,即便我們沒有主動去創建執行緒,行程也是默認有一個主執行緒的,
守護執行緒(Daemon Thread)
有的時候應用中需要一個長期駐留的服務程式,但是不希望這個服務程式影回應用退出,那么我們就可以將這個服務程式設定為守護執行緒,如果 Java 虛擬機發現只有守護執行緒存在時,將結束行程,
在 Java 中將執行緒設定為守護執行緒,具體的實作代碼如下所示:
public static void main(String[] args) {
Thread daemonThread = new Thread();
// 必須在執行緒啟動之前設定
daemonThread.setDaemon(true);
daemonThread.start();
}
通用的執行緒生命周期
在作業系統層面,執行緒有生命周期,
對于有生命周期的事物,要學好它,只要能搞懂生命周期中各個節點的狀態轉換機制就可以了,
通用的執行緒生命周期基本上可以用下圖這個 “五態模型” 來描述,這五態分別是:初始狀態、可運行狀態、運行狀態、休眠狀態和終止狀態,

這“五態模型”的詳細情況如下所示,
初始狀態
初始狀態,指的是執行緒已經被創建,但是還不允許被 CPU 調度,
初始狀態屬于編程語言特有的,這里所謂的被創建,僅僅是在編程語言層面被創建,而在作業系統層面,真正的執行緒還沒有被創建,
在 Java 中,初始狀態相當于是創建了 Thread 類的物件,但是還沒有呼叫 Thread#start() 方法,
可運行狀態
可運行狀態,指的是執行緒可以被作業系統調度,但是執行緒還沒有開始執行,
在可運行狀態下,真正的作業系統執行緒已經被創建,多個執行緒處于可運行狀態時,作業系統會根據調度演算法選擇一個執行緒運行,
在 Java 中,可運行狀態相當于是呼叫了 Thread#start() 方法,但是執行緒還沒有被分配 CPU 執行,
運行狀態
當有空閑的 CPU 時,作業系統會將空閑的 CPU 分配給一個處于可運行狀態的執行緒,被分配到 CPU 的執行緒的狀態就從可運行狀態轉換成了運行狀態,
在 Java 中,運行狀態相當于是呼叫了 Thread#start() 方法,并且執行緒被分配 CPU 執行,
休眠狀態
如果運行狀態的執行緒呼叫了一個阻塞的 API(例如以阻塞的方式讀取檔案)或者等待某個事件(例如條件變數),那么執行緒的狀態就會從運行狀態轉換到休眠狀態,同時釋放 CPU 的使用權,休眠狀態的執行緒永遠沒有機會獲得 CPU 的使用權,
當等待的資源或條件滿足后,執行緒就會從休眠狀態轉換到可運行狀態,并等待 CPU 調度,
終止狀態
執行緒執行完畢或者出現例外,執行緒就會進入終止狀態,即執行緒的生命周期終止,
這五種狀態在不同編程語言里會有簡化合并,例如:
- C 語言的 POSIX Threads 規范,就把初始狀態和可運行狀態合并了;
- Java 程式設計語言把可運行狀態和運行狀態合并了,這兩個狀態在作業系統調度層面有用,而 Java 虛擬機層面不關心這兩個狀態,因為 Java 虛擬機把執行緒調度交給作業系統處理了,
除了簡化合并,這五種狀態也有可能被細化,比如,Java 語言里就細化了休眠狀態(這個下面我們會詳細講解),
Java 的執行緒生命周期
不同的程式設計語言對于作業系統執行緒進行了不同的封裝,下面我們學習一下 Java 的執行緒生命周期,
Java 程式設計語言中,執行緒共有六種狀態,分別是:
- NEW(初始狀態)
- RUNNABLE(可運行 / 運行狀態)
- BLOCKED(阻塞狀態)
- WAITING(無時限等待)
- TIMED_WAITING(有時限等待)
- TERMINATED(終止狀態)
NEW(初始狀態)、TERMINATED(終止狀態)和通用的執行緒生命周期中的語意相同,
在作業系統層面,Java 執行緒中的 BLOCKED、WAITING、TIMED_WAITING 是一種狀態,即通用的執行緒生命周期中的休眠狀態,也就是說只要 Java 執行緒處于這三種狀態之一,那么這個執行緒就永遠沒有機會獲得 CPU 的使用權,
所以 Java 中的執行緒生命周期可以簡化為下圖:

其中,可以將 BLOCKED、WAITING、TIMED_WAITING 理解為導致執行緒處于休眠狀態的三種原因,
- 那具體是哪些情形會導致執行緒從 RUNNABLE 狀態轉換到這三種狀態呢?
- 而這三種狀態又是何時轉換回 RUNNABLE 的呢?
- 以及 NEW、TERMINATED 和 RUNNABLE 狀態是如何轉換的?
下面我們詳細講解,
Java 的執行緒狀態切換
從 NEW 到 RUNNABLE 狀態
剛創建 Thread 類的物件時,執行緒處于 NEW 狀態,
NEW 狀態的執行緒,不會被作業系統調度,因此不會執行,Java 執行緒要執行,就必須轉換到 RUNNABLE 狀態,
從 NEW 狀態轉換到 RUNNABLE 狀態只要呼叫執行緒物件的 start() 方法就可以了,具體的實作代碼如下所示:
public static void main(String[] args) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("hello");
}
});
thread.start();
}
從 RUNNABLE 到 TERMINATED 狀態
執行緒執行完 Thrad#run() 方法后,會自動從 RUNNABLE 狀態轉換到 TERMINATED 狀態,
如果執行 run() 方法的時候例外了拋出,也會導致執行緒終止,進入 TERMINATED 狀態 ,
1. RUNNABLE 與 BLOCKED 的狀態轉換
只有一種場景會觸發 RUNNABLE 與 BLOCKED 的狀態轉換,就是執行緒等待 synchronized 的隱式鎖,
- 當使用 synchronized 申請加鎖失敗時,該執行緒的狀態就會從 RUNNABLE 轉換到 BLOCKED 狀態,
- 當等待的執行緒獲得鎖時,該執行緒的狀態就會從 BLOCKED 狀態轉換到 RUNNABLE 狀態,
如果你熟悉作業系統執行緒的生命周期的話,可能會有個疑問:執行緒呼叫阻塞式 API 時,是否會轉換到 BLOCKED 狀態呢?在作業系統層面,執行緒是會轉換到休眠狀態的,但是在 Java 虛擬機層面,Java 執行緒的狀態不會發生變化,也就是說 Java 執行緒的狀態會依然保持 RUNNABLE 狀態,
Java 虛擬機層面并不關心作業系統調度相關的狀態,因為在 Java 虛擬機看來,等待 CPU 的使用權(作業系統層面此時處于可執行狀態)與等待 I/O(作業系統層面此時處于休眠狀態)沒有區別,都是在等待某個資源,所以都歸入了 RUNNABLE 狀態,
而我們說的 Java 執行緒在呼叫阻塞式 API 時,執行緒會阻塞,指的是作業系統執行緒的狀態,并不是 Java 執行緒的狀態,
2. RUNNABLE 與 WAITING 的狀態轉換
總體來說,有三種場景會觸發 RUNNABLE 與 WAITING 的狀態轉換,
第一種場景,獲得 synchronized 隱式鎖的執行緒,呼叫無引數的 Object#wait() 方法,
這里應該呼叫的是鎖物件的 wait() 方法,具體的實作代碼如下所示:
public void method() throws InterruptedException {
synchronized (this) {
this.wait();
}
}
- 當呼叫 wait() 方法時,呼叫方法的執行緒的狀態從 RUNNABLE 狀態轉換到 WAITING 狀態
- 當呼叫 notify() 方法時,被喚醒的執行緒的狀態從 WAITING 狀態轉換到 RUNNABLE 狀態
第二種場景,呼叫無引數的 Thread#join() 方法,
join() 是一種執行緒同步方法,例如有一個執行緒物件 thread A:
- 當呼叫 A.join() 方法時,執行這條陳述句的執行緒會等待 thread A 執行完,而等待中的這個執行緒,其狀態會從 RUNNABLE 轉換到 WAITING,
- 當執行緒 thread A 執行完,原來等待它的執行緒又會從 WAITING 狀態轉換到 RUNNABLE,
Thread#join() 方法的實作基于 Object#wait(),
第三種場景,呼叫 LockSupport#park() 方法,
LockSupport 類,也許你有點陌生,其實 Java 并發包中鎖的實作都用到了 LockSupport#park() / unpark(),
- 當呼叫 LockSupport.park() 方法時,呼叫方法的執行緒的狀態從 RUNNABLE 轉換到 WAITING,
- 當呼叫 LockSupport.unpark(Thread thread) 方法時,被喚醒的執行緒的狀態從 WAITING 狀態轉換到 RUNNABLE 狀態
總結來說:Object#wait() 和 LockSupport#park() 方法使執行緒的狀態轉換到 WAITING,
3. RUNNABLE 與 TIMED_WAITING 的狀態轉換
總體來說,有五種場景會觸發 RUNNABLE 與 TIMED_WAITING 的狀態轉換:
- 獲得 synchronized 隱式鎖的執行緒,呼叫帶超時引數的 Object#wait(long timeout) 方法;
- 呼叫帶超時引數的 Thread#join(long millis) 方法;(底層呼叫 Object#wait(long timeout) )
- 呼叫帶超時引數的 LockSupport.parkNanos(Object blocker, long deadline) 方法;
- 呼叫帶超時引數的 LockSupport.parkUntil(long deadline) 方法,
- 呼叫帶超時引數的 Thread.sleep(long millis) 方法;
這里你會發現:
- TIMED_WAITING 和 WAITING 狀態的區別,僅僅是觸發條件多了超時引數,
- 與 RUNNABLE 與 WAITING 的狀態轉換 相比,多了一個 Thread.sleep() 場景,
Java 執行緒 API 的使用
執行緒的創建
創建執行緒的幾種方式:
- 繼承 Thread 類,重寫 run() 方法,
- 實作 Runnable 介面,實作其中的 run() 方法,將該實作類的物件作為引數傳遞到 Thread 類的構造器中,創建 Thread 類的物件,
- 實作 Callable 介面,實作其中的 call() 方法,將該實作類的物件作為引數傳遞到 FutureTask 類的構造器中,創建FutureTask 類的物件,將 FutureTask 類的物件作為引數傳遞到 Thread 類的構造器中,創建 Thread 類的物件,Callable 它解決了 Runnable 無法回傳結果的困擾,
「實作 Runnable 介面」VS「繼承 Thread 類」
- 通過實作(implements)的方式沒有類的單繼承性的局限性
- 實作的方式更適合處理多個執行緒有共享資料的情況
「實作 Callable 介面」VS「實作 Runnable 介面」
- call() 可以有回傳值
- call() 可以拋出例外被外面的操作捕獲,獲取例外的資訊
- 「實作 Callable 介面」支持泛型
// 自定義執行緒物件
class MyThread extends Thread {
public void run() {
// 執行緒需要執行的代碼
......
}
}
// 創建執行緒物件
MyThread myThread = new MyThread();
// 實作Runnable介面
class Runner implements Runnable {
@Override
public void run() {
// 執行緒需要執行的代碼
......
}
}
// 創建執行緒物件
Thread thread = new Thread(new Runner());
public static void main(String[] args) throws ExecutionException, InterruptedException {
MyTask task = new MyTask();
// FutureTask 用于接收運算結果
FutureTask futureTask = new FutureTask<>(task);
Thread thread = new Thread(futureTask);
thread.start();
// FutureTask 可用于執行緒間同步 (當前執行緒等待其他執行緒執行完成之后,當前執行緒才繼續執行)
// get() 回傳值即為 FutureTask 構造器引數 Callable 實作類實作的 call() 的回傳值
System.out.println(futureTask.get());
}
public class MyTask implements Callable {
@Override
public String call() {
// 若不需要回傳值,可 return null;
return "ok";
}
}
執行緒的執行
創建好 Thread 類的物件后,通過呼叫 Thread#start() 方法創建執行緒執行任務,
執行緒執行要呼叫 start() 而不是直接呼叫 run(),直接呼叫 run() 方法只會在當前執行緒上同步執行 run() 方法的內容,而不會啟動新執行緒,呼叫 start() 方法的作用:
- 啟動一個新的執行緒
- 新的執行緒呼叫 run() 方法
執行緒的停止
有時候我們需要強制中斷 run() 方法的執行,例如 run() 方法訪問一個很慢的網路,我們等不下去了,想終止怎么辦呢?Java 的 Thread 類里面倒是有個 stop() 方法,不過已經標記為 @Deprecated,所以不建議使用了,正確的方式是呼叫 interrupt() 方法,Thread#interrupt() 配合合適的代碼,即可優雅的實作執行緒的終止,
stop() 和 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 狀態時:
- 當執行緒 A 處于 RUNNABLE 狀態,并且阻塞在 java.nio.channels.InterruptibleChannel 上時,如果其他的執行緒呼叫執行緒 A 的 interrupt() 方法,執行緒 A 會觸發 java.nio.channels.ClosedByInterruptException 這個例外;
- 當執行緒 A 處于 RUNNABLE 狀態,并且阻塞在 java.nio.channels.Selector 上時,如果其他的執行緒呼叫執行緒 A 的 interrupt() 方法,執行緒 A 的 java.nio.channels.Selector 會立即回傳,
上面這兩種情況屬于被中斷的執行緒通過例外的方式獲得了通知,
主動檢測
還有一種是主動檢測,如果執行緒處于 RUNNABLE 狀態,并且沒有阻塞在某個 I/O 操作上,例如中斷計算圓周率的執行緒 A,這時就得依賴執行緒 A 主動檢測中斷狀態了,如果其他的執行緒呼叫執行緒 A 的 interrupt() 方法,那么執行緒 A 可以通過 isInterrupted() 方法,檢測是不是自己被中斷了,
參考資料
第17講 | 一個執行緒兩次呼叫start()方法會出現什么情況?-極客時間 (geekbang.org)
09 | Java執行緒(上):Java執行緒的生命周期 (geekbang.org)
06 | 執行緒池基礎:如何用執行緒池設計出更“優美”的代碼? (geekbang.org)
11 | 執行緒:如何讓復雜的專案并行執行?-極客時間 (geekbang.org)
本文來自博客園,作者:真正的飛魚,轉載請注明原文鏈接:https://www.cnblogs.com/feiyu2/p/thread.html
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/552417.html
標籤:其他
上一篇:如何優雅得關閉協程呢
下一篇:返回列表
