Java 執行緒基礎知識
前言
什麼是執行緒?執行緒,有時被稱為輕量程序(Lightweight Process,LWP),是程式執行流的最小單元。一個標準的執行緒由執行緒 ID,當前指令指標 (PC),暫存器集合和堆疊組成。另外,執行緒是程序中的一個實體,是被系統獨立排程和分派的基本單位,執行緒自己不擁有系統資源,只擁有一點兒在執行中必不可少的資源,但它可與同屬一個程序的其它執行緒共享程序所擁有的全部資源。
一個執行緒可以建立和撤消另一個執行緒,同一程序中的多個執行緒之間可以併發執行。由於執行緒之間的相互制約,致使執行緒在執行中呈現出間斷性。執行緒也有就緒、阻塞和執行三種基本狀態。就緒狀態是指執行緒具備執行的所有條件,邏輯上可以執行,在等待處理機;執行狀態是指執行緒佔有處理機正在執行;阻塞狀態是指執行緒在等待一個事件(如某個訊號量),邏輯上不可執行。每一個程式都至少有一個執行緒,若程式只有一個執行緒,那就是程式本身。
程序 VS 執行緒
程序和執行緒是包含關係,但是多工既可以由多程序實現,也可以由執行緒實現,還可以混合多程序+多執行緒。
和多執行緒相比,多程序的缺點是:
-
建立程序比建立執行緒開銷大很多,尤其是在 Windows 上
-
程序間通訊比執行緒要慢,因為執行緒見通訊就是讀寫同一個變數,速度很快
多程序的優點:
- 多程序穩定性比多執行緒高,因為在多程序情況下,一個程序的崩潰不會影響其他程序,任何一個執行緒崩潰會導致整個程序崩潰。
多執行緒的應用場景
-
程式中出現需要等待的操作,比如網路操作、檔案 IO 等,可以利用多執行緒充分使用處理器資源,而不會阻塞程式中其他任務的執行
-
程式中出現可分解的大任務,比如耗時較長的計算任務,可以利用多執行緒來共同完成任務,縮短運算時間
-
程式中出現需要後臺執行的任務,比如一些監測任務、定時任務,可以利用多執行緒來完成
生命週期及五種基本狀態
首先,看一下 Thread 類中給出的關於執行緒狀態的說明:
public enum State { //還沒有呼叫start()開啟的執行緒例項所處的狀態 NEW, //正在虛擬機器中執行或者等待被執行的執行緒所處的狀態,但是這種狀態也包含執行緒正在等待處理器資源這種情況 RUNNABLE, // 等待在監視器鎖上的執行緒所處的狀態,比如進入synchronized同步程式碼塊或同步方法失敗 BLOCKED, // 等待其它執行緒執行特定操作的執行緒所處的狀態;比如執行緒執行了以下方法: Object.wait with no timeout、Thread.join with no timeout、 LockSupport.park WAITING, // 等待其它執行緒執行超時操作的執行緒所處的狀態;比如執行緒執行了以下方法: Thread.sleep、Object.wait with timeout //Thread.join with timeout、LockSupport.parkNanos、LockSupport.parkUntil TIMED_WAITING, //退出的執行緒所處的狀態 TERMINATED; }
接下來在看一下 Java 中執行緒的生命週期較為經典的圖:
上圖中基本上囊括了 Java 中多執行緒各重要知識點。掌握了上圖中的各知識點,Java 中的多執行緒也就基本上掌握了。主要包括:
Java 執行緒具有五中基本狀態
-
新建狀態(New):當執行緒物件對建立後,即進入了新建狀態,如:Thread t = new MyThread();
-
就緒狀態(Runnable):當呼叫執行緒物件的 start() 方法(t.start();),執行緒即進入就緒狀態。處於就緒狀態的執行緒,只是說明此執行緒已經做好了準備,隨時等待 CPU 排程執行,並不是說執行了 t.start() 此執行緒立即就會執行;
-
執行狀態(Running):當 CPU 開始排程處於就緒狀態的執行緒時,此時執行緒才得以真正執行,即進入到執行狀態。注:就緒狀態是進入到執行狀態的唯一入口,也就是說,執行緒要想進入執行狀態執行,首先必須處於就緒狀態中;
-
阻塞狀態(Blocked):處於執行狀態中的執行緒由於某種原因,暫時放棄對 CPU 的使用權,停止執行,此時進入阻塞狀態,直到其進入到就緒狀態,才有機會再次被 CPU 呼叫以進入到執行狀態。根據阻塞產生的原因不同,阻塞狀態又可以分為三種:
-
等待阻塞:執行狀態中的執行緒執行 wait() 方法,使本執行緒進入到等待阻塞狀態;
-
同步阻塞 -- 執行緒在獲取 synchronized 同步鎖失敗(因為鎖被其它執行緒所佔用),它會進入同步阻塞狀態;
-
其他阻塞 -- 通過呼叫執行緒的 sleep() 或 join() 或發出了 I/O 請求時,執行緒會進入到阻塞狀態。當 sleep() 狀態超時、join() 等待執行緒終止或者超時、或者 I/O 處理完畢時,執行緒重新轉入就緒狀態。
-
死亡狀態(Dead):執行緒執行完了或者因異常退出了 run() 方法,該執行緒結束生命週期。
舉個通俗一點的例子來解釋上面五種狀態,比如上廁所:
你平時去商城上廁所,準備去上廁所就是新建狀態(new),上廁所要排隊,排隊就是就緒狀態(Runnable),有坑位了,輪到你了,拉屎就是執行狀態(Running),你拉完屎發現沒有手紙,要等待別人給你送紙過來,這個狀態就是阻塞(Blocked),等你上完廁所出來,上廁所這件事情結束了就是死亡狀態了。
注意:便祕也是阻塞狀態,你便祕太久了,別人等不及了,把你趕走,這個就是掛起,還有一種情況,你便祕了,別人等不及了,跟你說你先出去醞釀一下,5分鐘後再過來拉屎,這就是睡眠。
自定義執行緒的實現
處於實用的角度出發,想要使用多執行緒,那麼第一步就是需要知道如何實現自定義執行緒,因為實際開發中,需要執行緒完成的任務是不同的,所以我們需要根據執行緒任務來自定義執行緒,JDK 為我們的開發人員提供了三種自定義執行緒的方式,供實際開發中使用,來開發出符合需求的多執行緒程式!
以下是執行緒的三種實現方式,以及對每種實現的優缺點進行分析,最後是對這三種實現方式進行總結;
方式一:繼承Thread類
// 通過繼承Thread類實現自定義執行緒類 public class MyThread extends Thread { // 執行緒體 @Override public void run() { System.out.println("Hello, I am the defined thread created by extends Thread"); } public static void main(String[] args){ // 例項化自定義執行緒類例項 Thread thread = new MyThread(); // 呼叫start()例項方法啟動執行緒 thread.start(); } }
優點:實現簡單,只需例項化繼承類的例項,即可使用執行緒
缺點:擴充套件性不足,Java是單繼承的語言,如果一個類已經繼承了其他類,就無法通過這種方式實現自定義執行緒
方式二:實現 Runnable 介面
public class MyRunnable implements Runnable { // 執行緒體 @Override public void run() { System.out.println("Hello, I am the defined thread created by implements Runnable"); } public static void main(String[] args){ // 執行緒的執行目標物件 MyRunnable myRunnable = new MyRunnable(); // 實際的執行緒物件 Thread thread = new Thread(myRunnable); // 啟動執行緒 thread.start(); } }
優點:
- 擴充套件性好,可以在此基礎上繼承其他類,實現其他必需的功能
- 對於多執行緒共享資源的場景,具有天然的支援,適用於多執行緒處理一份資源的場景
缺點:構造執行緒例項的過程相對繁瑣一點
方式三:實現Callable介面
package com.thread; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.FutureTask; public class MyCallable implements Callable<String> { @Override public String call() throws Exception { return "Hello, I am the defined thread created by implements Callable"; } public static void main(String[] args){ // 執行緒執行目標 MyCallable myCallable = new MyCallable(); // 包裝執行緒執行目標,因為Thread的建構函式只能接受Runnable介面的實現類,而FutureTask類實現了Runnable介面 FutureTask<String> futureTask = new FutureTask<>(myCallable); // 傳入執行緒執行目標,例項化執行緒物件 Thread thread = new Thread(futureTask); // 啟動執行緒 thread.start(); String result = null; try { // 獲取執行緒執行結果 result = futureTask.get(); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } System.out.println(result); } }
優點:
- 擴充套件性好
- 支援多執行緒處理同一份資源
- 具備返回值以及可以丟擲受檢查異常
缺點:
- 相較於實現Runnable介面的方式,較為繁瑣
執行緒常用方法簡單介紹
sleep()
是一個靜態方法。使當前執行緒(即呼叫該方法的執行緒)暫停執行一段時間,讓其他執行緒有機會繼續執行,但它並不釋放物件鎖,當到達指定的睡眠時間後會返回,執行緒處於就緒狀態,然後參與CPU排程。也就是如果有 Synchronized 同步塊,其他執行緒仍然不同訪問共享資料。注意該方法要捕獲異常
比如有兩個執行緒同時執行(沒有 Synchronized ),一個執行緒優先順序為 MAX_PRIORITY,另一個為 MIN_PRIORITY,如果沒有 sleep() 方法,只有高優先順序的執行緒執行完成後,低優先順序的執行緒才能執行,但當高優先順序的執行緒 sleep(5000) 後,低優先順序就有機會執行了。
總之,sleep() 可以使低優先順序的執行緒得到執行的機會,當然也可以讓同優先順序、高優先順序的執行緒有執行的機會。
join()
Thread 類中有一個 join() 方法,非靜態方法。此方法表示,在當前執行緒中 a 中,b 執行緒呼叫 join() 方法,那麼,a 執行緒就會釋放資源,讓給 b 執行緒先執行。。注意該方法也要捕獲異常。
yield()
是一個靜態方法,與 sleep() 類似,只是不能由使用者指定暫停多長時間,呼叫該方法會讓當前執行緒讓出CPU使用權,然後處於就緒狀態,執行緒排程會從就緒佇列裡面獲取一個優先順序最高的執行緒,也可能會排程到剛剛讓出CPU的那個執行緒繼續獲取CPU執行權。
wait() 和notify()、notifyAll()
這三個方法用於協調多個執行緒對共享資料的存取,所以必須在 Synchronized 語句塊內使用這三個方法。前面說過Synchronized 這個關鍵字用於保護共享資料,阻止其他執行緒對共享資料的存取。但是這樣程式的流程就很不靈活了,如何才能在當前執行緒還沒退出 Synchronized 資料塊時讓其他執行緒也有機會訪問共享資料呢?此時就用這三個方法來靈活控制。
wait() 方法使當前執行緒被阻塞掛起暫停執行並釋放物件鎖標誌,讓其他執行緒可以進入 Synchronized 資料塊,當前執行緒被放入物件等待池中。當呼叫共享物件的 notify() 或者 notifyAll() 方法才會返回,此時返回的執行緒會加入到鎖標誌等待池中,只有鎖標誌等待池中的執行緒能夠獲取鎖標誌,如果鎖標誌等待池中沒有執行緒,則 notify() 不起作用。
notifyAll() 則從物件等待池中移走所有等待那個物件的執行緒並放到鎖標誌等待池中。
需要注意的是,當執行緒呼叫共享物件的 wait() 方法時,當前執行緒只會釋放當前共享物件的鎖,當前執行緒持有的其他共享物件的監視器鎖並不會釋放。
執行緒中斷
為啥需要中斷呢?下面簡單的舉例情況:
-
比如我們會啟動多個執行緒做同一件事,比如搶 12306 的火車票,我們可能開啟多個執行緒從多個渠道買火車票,只要有一個渠道買到了,我們會通知取消其他渠道。這個時候需要關閉其他執行緒;
-
很多執行緒的執行模式是死迴圈,比如在生產者/消費者模式中,消費者主體就是一個死迴圈,它不停的從佇列中接受任務,執行任務,在停止程式時,我們需要一種”優雅”的方法以關閉該執行緒;
-
在一些場景中,比如從第三方伺服器查詢一個結果,我們希望在限定的時間內得到結果,如果得不到,我們會希望取消該任務;
上面這幾個例子執行緒已經在運行了,並不好去幹涉,但是可以通過中斷,告訴這個執行緒,你應該中斷了。比如上面的例子中的執行緒再收到中斷後,可以通過中斷標誌來結束執行緒的執行。當然,你也可以收到後,不做任何處理,這也是可以的。
在 Java 中,停止一個執行緒的主要機制是中斷,中斷並不是強迫終止一個執行緒,它是一種協作機制,是給執行緒傳遞一個取消訊號,但是由執行緒來決定如何以及何時退出。
需要注意的是:在停止執行緒的時候,不要呼叫 stop 方法,該方法已經被廢棄了,並且會帶來不可預測的影響。
執行緒對中斷的反應
-
RUNNABLE:執行緒在執行或具備執行條件只是在等待作業系統排程
-
WAITING/TIMED_WAITING:執行緒在等待某個條件或超時
-
BLOCKED:執行緒在等待鎖,試圖進入同步塊
-
NEW/TERMINATED:執行緒還未啟動或已結束
RUNNABLE 狀態
如果執行緒在執行中,interrupt() 只是會設定執行緒的中斷標誌位,沒有任何其它作用。執行緒應該在執行過程中合適的位置檢查中斷標誌位,比如說,如果主體程式碼是一個迴圈,可以在迴圈開始處進行檢查,如下所示:
public class InterruptRunnableDemo extends Thread { @Override public void run() { while (!Thread.currentThread().isInterrupted()) { // ... 單次迴圈程式碼 } System.out.println("done "); } public static void main(String[] args) throws InterruptedException { Thread thread = new InterruptRunnableDemo(); thread.start(); Thread.sleep(1000); thread.interrupt(); } }
WAITING/TIMED_WAITING
執行緒執行如下方法會進入WAITING狀態:
public final void join() throws InterruptedException public final void wait() throws InterruptedException
執行如下方法會進入TIMED_WAITING狀態:
public final native void wait(long timeout) throws InterruptedException; public static native void sleep(long millis) throws InterruptedException; public final synchronized void join(long millis) throws InterruptedException
在這些狀態時,對執行緒物件呼叫 interrupt() 會使得該執行緒丟擲 InterruptedException,需要注意的是,丟擲異常後,中斷標誌位會被清空(執行緒的中斷標誌位會由 true 重置為false,因為執行緒為了處理異常已經重新處於就緒狀態),而不是被設定。比如說,執行如下程式碼:
Thread t = new Thread (){ @Override public void run() { try { Thread.sleep(1000); } catch (InterruptedException e) { //exception被捕獲,但是為輸出為false 因為標誌位會被清空 System.out.println(isInterrupted()); } } }; t.start(); try { Thread.sleep(100); } catch (InterruptedException e) { } t.interrupt();//置為true
InterruptedException 是一個受檢異常,執行緒必須進行處理。我們在異常處理中介紹過,處理異常的基本思路是,如果你知道怎麼處理,就進行處理,如果不知道,就應該向上傳遞,通常情況下,你不應該做的是,捕獲異常然後忽略。
捕獲到 InterruptedException,通常表示希望結束該執行緒,執行緒大概有兩種處理方式:
-
向上傳遞該異常,這使得該方法也變成了一個可中斷的方法,需要呼叫者進行處理
-
有些情況,不能向上傳遞異常,比如Thread的run方法,它的宣告是固定的,不能丟擲任何受檢異常,這時,應該捕獲異常,進行合適的清理操作,清理後,一般應該呼叫Thread的interrupt方法設定中斷標誌位,使得其他程式碼有辦法知道它發生了中斷
第一種方式的示例程式碼如下:
//丟擲中斷異常,由呼叫者捕獲 public void interruptibleMethod() throws InterruptedException{ // ... 包含wait, join 或 sleep 方法 Thread.sleep(1000); }
第二種方式的示例程式碼如下:
public class InterruptWaitingDemo extends Thread { @Override public void run() { while (!Thread.currentThread().isInterrupted()) { try { // 模擬任務程式碼 Thread.sleep(2000); } catch (InterruptedException e) { // ... 清理操作 System.out.println(isInterrupted());//false // 重設中斷標誌位為true Thread.currentThread().interrupt(); } } System.out.println(isInterrupted());//true } public static void main(String[] args) { InterruptWaitingDemo thread = new InterruptWaitingDemo(); thread.start(); try { Thread.sleep(100); } catch (InterruptedException e) { } thread.interrupt(); } }
BLOCKED
如果執行緒在等待鎖,對執行緒物件呼叫interrupt()只是會設定執行緒的中斷標誌位,執行緒依然會處於BLOCKED狀態,也就是說,interrupt()並不能使一個在等待鎖的執行緒真正”中斷”。我們看段程式碼:
public class InterruptWaitingDemo extends Thread { @Override public void run() { while (!Thread.currentThread().isInterrupted()) { try { // 模擬任務程式碼 Thread.sleep(2000); } catch (InterruptedException e) { // ... 清理操作 // 重設中斷標誌位 Thread.currentThread().interrupt(); } } System.out.println(isInterrupted()); } public static void main(String[] args) { InterruptWaitingDemo thread = new InterruptWaitingDemo(); thread.start(); try { Thread.sleep(100); } catch (InterruptedException e) { } thread.interrupt(); } }
BLOCKED 如果執行緒在等待鎖,對執行緒物件呼叫 interrupt() 只是會設定執行緒的中斷標誌位,執行緒依然會處於 BLOCKED 狀態,也就是說,interrupt() 並不能使一個在等待鎖的執行緒真正”中斷”。我們看段程式碼:
public class InterruptSynchronizedDemo { private static Object lock = new Object();//monitor private static class A extends Thread { @Override public void run() { //等待lock鎖 synchronized (lock) { //等待標誌位被置為true while (!Thread.currentThread().isInterrupted()) { } } System.out.println("exit"); } } public static void test() throws InterruptedException { synchronized (lock) {//獲取鎖 A a = new A(); a.start(); Thread.sleep(1000); //a在等待lock鎖,interrupt 無法中斷 a.interrupt(); //a執行緒加入當前執行緒,等待執行完畢 a.join(); } } public static void main(String[] args) throws InterruptedException { test(); } }
test 方法在持有鎖 lock 的情況下啟動執行緒 a,而執行緒 a 也去嘗試獲得鎖 lock,所以會進入鎖等待佇列,隨後 test 呼叫執行緒 a 的 interrupt 方法並等待執行緒執行緒 a 結束,執行緒 a 會結束嗎?不會,interrupt 方法只會設定執行緒的中斷標誌,而並不會使它從鎖等待佇列中出來。執行緒a 會一直嘗試獲取鎖,但是主執行緒也在等待 a 結束才會釋放鎖,所以相互之間互為等待,不能結束。
我們稍微修改下程式碼,去掉 test方法中的最後一行 a.join(),即變為:
public static void test() throws InterruptedException { synchronized (lock) { A a = new A(); a.start(); Thread.sleep(1000); a.interrupt(); } //lock鎖釋放後 A執行緒重佇列中出來 }
這時,程式就會退出。為什麼呢?因為主執行緒不再等待執行緒 a 結束,釋放鎖 lock 後,執行緒 a 會獲得鎖,然後檢測到發生了中斷,所以會退出。
在使用 synchronized 關鍵字獲取鎖的過程中不響應中斷請求,這是 synchronized 的侷限性。如果這對程式是一個問題,應該使用顯式鎖,java 中的 Lock 介面,它支援以響應中斷的方式獲取鎖。對於 Lock.lock(),可以改用 Lock.lockInterruptibly(),可被中斷的加鎖操作,它可以丟擲中斷異常。等同於等待時間無限長的 Lock.tryLock(long time, TimeUnit unit)。
NEW/TERMINATE
如果執行緒尚未啟動 (NEW),或者已經結束 (TERMINATED),則呼叫 interrupt() 對它沒有任何效果,中斷標誌位也不會被設定。比如說,以下程式碼的輸出都是 false。
public class InterruptNotAliveDemo { private static class A extends Thread { @Override public void run() { } } public static void test() throws InterruptedException { A a = new A(); a.interrupt(); System.out.println(a.isInterrupted()); a.start(); Thread.sleep(100); a.interrupt(); System.out.println(a.isInterrupted()); } public static void main(String[] args) throws InterruptedException { test(); } }
IO操作
如果執行緒在等待 IO 操作,尤其是網路 IO,則會有一些特殊的處理,我們沒有介紹過網路,這裡只是簡單介紹下。
-
實現此 InterruptibleChannel 介面的通道是可中斷的:如果某個執行緒在可中斷通道上因呼叫某個阻塞的 I/O 操作(常見的操作一般有這些:serverSocketChannel. accept()、socketChannel.connect、socketChannel.open、socketChannel.read、socketChannel.write、fileChannel.read、fileChannel.write)而進入阻塞狀態,而另一個執行緒又呼叫了該阻塞執行緒的 interrupt 方法,這將導致該通道被關閉,並且已阻塞執行緒接將會收到 ClosedByInterruptException,並且設定已阻塞執行緒的中斷狀態。另外,如果已設定某個執行緒的中斷狀態並且它在通道上呼叫某個阻塞的 I/O 操作,則該通道將關閉並且該執行緒立即接收到 ClosedByInterruptException;並仍然設定其中斷狀態。
-
如果執行緒阻塞於 Selector 呼叫,則執行緒的中斷標誌位會被設定,同時,阻塞的呼叫會立即返回。
我們重點介紹另一種情況,InputStream 的 read 呼叫,該操作是不可中斷的,如果流中沒有資料,read 會阻塞 (但執行緒狀態依然是 RUNNABLE ),且不響應 interrupt(),與 synchronized 類似,呼叫 interrupt() 只會設定執行緒的中斷標誌,而不會真正”中斷”它,我們看段程式碼
public class InterruptReadDemo { private static class A extends Thread { @Override public void run() { while(!Thread.currentThread().isInterrupted()){ try { System.out.println(System.in.read())//wait input } catch (IOException e) { e.printStackTrace(); } } System.out.println("exit"); } } public static void main(String[] args) throws InterruptedException { A t = new A(); t.start(); Thread.sleep(100); t.interrupt(); } }
執行緒t啟動後呼叫 System.in.read() 從標準輸入讀入一個字元,不要輸入任何字元,我們會看到,呼叫 interrupt() 不會中斷 read(),執行緒會一直執行。
不過,有一個辦法可以中斷 read() 呼叫,那就是呼叫流的 close 方法,我們將程式碼改為:
public class InterruptReadDemo { private static class A extends Thread { @Override public void run() { while (!Thread.currentThread().isInterrupted()) { try { System.out.println(System.in.read()); } catch (IOException e) { e.printStackTrace(); } } System.out.println("exit"); } public void cancel() { try { System.in.close(); } catch (IOException e) { } interrupt(); } } public static void main(String[] args) throws InterruptedException { A t = new A(); t.start(); Thread.sleep(100); t.cancel(); } }
我們給執行緒定義了一個 cancel 方法,在該方法中,呼叫了流的 close 方法,同時呼叫了 interrupt 方法,這次,程式會輸出:
-1 exit
也就是說,呼叫close方法後,read方法會返回,返回值為-1,表示流結束。
如何正確地取消/關閉執行緒
1. 以上,我們可以看出,interrupt 方法不一定會真正”中斷”執行緒,它只是一種協作機制,如果 不明白執行緒在做什麼,不應該貿然的呼叫執行緒的 interrupt 方法,以為這樣就能取消執行緒。
2. 對於以執行緒提供服務的程式模組而言,它應該封裝取消/關閉操作,提供單獨的取消/關閉方法給呼叫者,類似於 InterruptReadDemo 中演示的 cancel 方法,外部呼叫者應該呼叫這些方法而不是直接呼叫 interrupt。
3. Java併發庫的一些程式碼就提供了單獨的取消/關閉方法,比如說,Future介面提供瞭如下方法以取消任務:boolean cancel(boolean mayInterruptIfRunning);
4. 再比如,ExecutorService提供瞭如下兩個關閉方法:
void shutdown(); List<Runnable> shutdownNow();
5. Future 和 ExecutorService 的 API 文件對這些方法都進行了詳細說明,這是我們應該學習的方式。