1、概述
1.1 進程與線程
進程是程序運行時,操作系統進行資源分配的最小單位,包括 CPU、內存空間、磁盤 IO 等。從另一個角度講,進程是程序在設備(計算機、手機等)上的一次執行活動,或者說是正在運行中的程序。
一個程序進入內存運行時,它就變成一個進程。進程是處于運行中的程序,它擁有自己獨立的資源和地址空間,在沒有經過進程本身允許的情況下,進程不可以直接訪問其他進程的地址空間。同時多個進程可以在單個處理器上并發執行,多個進程之間互不影響。
線程是進程的一個實體,是 CPU 調度的最小單位。自己基本上不擁有系統資源,只擁有一點在運行中必不可少的資源(如程序計數器、一組寄存器和棧),但是它可與同屬一個進程的其他的線程共享進程所擁有的全部資源。
使用多線程解決了多任務同時運行的問題,并且,共享變量使得線程間的通信要比進程間通信更有效、更容易。此外,在一些操作系統中,與進程相比,線程更“輕量級”,創建、撤銷一個線程比啟動新進程的開銷要小得多。
當然,線程太多,來回切換也會導致執行效率降低。
其實應用程序的執行都是 CPU 在做著快速的切換完成的,這個切換是隨機的。在某一個時刻,CPU 只在執行一個線程,但是由于它的執行速度非常快,在毫秒級別,因此人無法感知到它在一個時間片中其實只在執行一個任務,在時間片結束后又去切換執行另一個任務。
1.2 并行與并發
并發是指在同一時刻只能有一條指令執行,但多個進程指令被快速輪換執行,使得宏觀上具有多個進程同時執行的效果;而并行是指在同一時刻,有多條指令在多個處理器上同時執行。
1.3 JVM 中的多線程解析
JVM 啟動時就啟動了多個線程,至少有兩個線程可以分析的出來:
- 執行 main 方法的線程,即主線程。該線程的任務代碼都定義在 main 方法中。主線程執行完任務,其所在進程也就關閉了。
- 負責垃圾回收的線程。系統會自己決定何時回收垃圾,你也可以通過 System.gc() 通知垃圾回收器來回收。但是這個也不是立即回收垃圾,也是在調用之后的一定時間內。另外 Object 中還有一個 finalize() 方法進行垃圾回收。
實際上,JVM 啟動的完整線程有以下這些:
- main:main線程,用戶程序入口
- Reference Handler:清除Reference的線程
- Finalizer:調用對象finalize方法的線程
- Signal Dispatcher:分發處理發送給JVM信號的線程
- Attach Listener:內存 dump,線程 dump,類信息統計,獲取系統屬性等
- Monitor Ctrl-Break:監控 Ctrl-Break 中斷信號的
此外,多線程運行時的示意圖如下:
說明:
- 當前有3個線程:main 線程、Thread-1、Thread-2,它們每個都維護了自己的方法棧(run 方法在棧底)。在哪個線程調用了方法,這個方法就會進入哪個線程。
- 如果 main 線程先于另外兩個線程執行完,JVM 不會結束,而是等所有線程都運行完。
- 在3個線程都運行的前提下,如果在某個線程中發生了異常導致該線程停止,不會影響其它線程,其它線程該怎么執行就怎么執行
2、使用
2.1 創建線程
創建線程的目的是為了開啟一條執行路徑,去運行指定的代碼(即該線程的任務)和其他代碼實現同時運行。
創建線程的方法有兩種:繼承 Thread 類和實現 Runnable 接口,實現 Callable 接口嚴格講是屬于第二種方式,不能單獨作為一種方法。
我們先來了解下 Callable 的基本用法再解釋上述觀點的原因。
Callable 的用法
Callable 是一個函數式接口,有泛型限制,該泛型參數類型與作為線程執行體的 call() 返回值類型相同。call() 比 Runnable 的 run() 功能要更強大,因為它可以有返回值,并且可以聲明拋出異常。
雖然 call() 也是線程執行體,但是由于 Callable 接口不是 Runnable 的子接口,所以 Callable 并不能直接作為 Thread 的 target,而是要借助 Future 接口。
Future 接口代表 call() 的返回值,而且 Future 的實現類 FutureTask 也實現了 Runnable 接口,可以作為 Thread 的 target。Future 中定義了如下的公共方法控制與它關聯的 Callable 任務:
創建并啟動有返回值的線程的步驟:
示例如下:
// Callable 后的參數類型為 String,意味著 call() 的返回值類型為 String
public class CallableTest implements Callable<String> {public static void main(String[] args) {new CallableTest().test();}/*** Callable 的 call() 類似于 Runnable 的 run(),只不過前者有返回值而前者沒有。** 此外,Future 接口可以控制 Runnable/Callable 取消執行任務、查詢任務是否完成、* 獲取任務執行結果(通過阻塞方法 get 獲取結果)。** 由于 Future 接口不能直接實例化,所以一般都是使用 FutureTask,它實現了 RunnableFuture* 接口,RunnableFuture 又繼承了 Runnable 和 Future,所以它既可以作為 Runnable 被線程執行,* 又可以作為Future得到Callable的返回值。*/private void test() {// 將 Callable 包裝進 FutureTask 后交給 ThreadFutureTask<String> futureTask = new FutureTask<>(this);new Thread(futureTask).start();try {System.out.println(futureTask.get());} catch (InterruptedException | ExecutionException e) {e.printStackTrace();}}// 任務是讀取文件內容,并發它轉換成字符串作為返回值@Overridepublic String call() throws Exception {StringBuffer stringBuffer = new StringBuffer();try (FileInputStream fileInputStream = new FileInputStream("filepath");FileChannel inChannel = fileInputStream.getChannel()) {ByteBuffer byteBuffer = ByteBuffer.allocate(256);while (inChannel.read(byteBuffer) != -1) {byteBuffer.flip();Charset charset = Charset.forName("GBK");CharBuffer charBuffer = charset.decode(byteBuffer);stringBuffer.append(charBuffer);byteBuffer.clear();}}return stringBuffer.toString();}
}
原因解釋
我們結合 Thread 的源碼解釋下原因。首先,Thread 的構造方法中并沒有 Callable 作為參數的:
只有空參和接收 Runnable 的構造方法:
public class Thread implements Runnable {/* What will be run. */private Runnable target;public Thread() {// nextThreadNum() 初始為 0,可以看到線程編號在創建線程時就已經確定init(null, null, "Thread-" + nextThreadNum(), 0);}public Thread(Runnable target) {// init() 會將參數 target 賦值給成員變量的 targetinit(null, target, "Thread-" + nextThreadNum(), 0);}public void run() {if (target != null) {target.run();}}
}
關注運行任務的 run():
- 如果使用繼承 Thread 的方式創建線程,那么 run() 被重寫,執行任務時就按照 Thread 子類的 run() 去執行。
- 如果使用實現 Runnable 的方式創建線程,run() 在判斷 target 不為空之后會運行 target 的 run()。可以認為 Runnable 就是對線程的任務進行了對象的封裝。
因為 FutureTask -> RunnableFuture -> Runnable & Future,而 Callable 需要交給 FutureTask 才能執行,所以實現 Callable 接口這種創建線程的方式在實現 Runnable 接口的范疇內,不能作為一種單獨的創建方式。
兩種創建線程的方式對比
兩種方式各有優缺點,通常還是使用實現 Runnable 接口的方式:
- 繼承 Thread 類方式編寫簡單如果要訪問當前線程無須使用 Thread.currentThread(),直接使用 this 即可。劣勢是不能再繼承其它父類。
- 實現 Runnable 接口方式則可以繼承其它類,并且多個線程可以共享同一個 target 對象,非常適合多個相同線程來處理同一份資源,從而將 CPU、代碼和數據分開,形成清晰的模型。缺點就是編程稍復雜,必須使用 Thread.currentThread() 訪問當前線程。
2.2 停止線程
線程會因為如下兩個原因之一被停止:
- run() 正常退出而自然死亡
- 因為一個沒有捕獲的的遺產終止了 run() 而意外死亡
推薦使用 Thread 的 interrupt() 配合 isInterrupted()/interrupted() 來停止線程。
interrupt()
interrupt() 會請求線程停止,注意是請求,而不是立即停止線程。該方法會將線程中的中斷狀態標記位
置位,等待線程通過 isInterrupted()/interrupted() 檢查該標記位來進行響應。isInterrupted() 和 interrupted() 都會在標記位被置位的情況下返回 true,不同點在于前者是對象方法,而后者是一個靜態方法,并且在調用后會將標記位改寫為 false。
在使用上述方法時需要注意,如果在中斷標記位為 true 的情況下執行阻塞方法(如 Thread.sleep()、Thread.join()、Object.wait()),這些阻塞方法會拋出 InterruptedException,并且在拋出異常后立即將線程的中斷標示位清除重置為 false:
public static void main(String[] args) throws InterruptedException {Thread thread = new Thread(new Runnable() {@Overridepublic void run() {while (!Thread.currentThread().isInterrupted()) {System.out.println("Thread is running...");try {Thread.sleep(500);} catch (InterruptedException e) {System.out.println(Thread.currentThread().isInterrupted());e.printStackTrace();// Thread.currentThread().interrupt();}}}});thread.start();Thread.sleep(1000);thread.interrupt();System.out.println("interrupt in main!");}
如果不打開 catch 中被注釋掉的 Thread.currentThread().interrupt(),線程是無法被中斷的,因為 sleep() 如果發現中斷標記位為 true 會拋出異常并將其清除為 false:
Thread is running...
Thread is running...
Thread is running...
Thread is running...
interrupt in main!
false
Thread is running...
java.lang.InterruptedException: sleep interruptedat java.lang.Thread.sleep(Native Method)at com.demo.thread.multi.InterruptDemo$1.run(InterruptDemo.java:13)at java.lang.Thread.run(Thread.java:748)
Thread is running...
Thread is running...
// 線程繼續運行輸出 Thread is running...
從這里也能看到子線程其實是在主線程結束后才消亡的,一定注意要讓線程能執行完,否則這個線程會阻止已經運行完的主線程所在的進程結束。
在拋出 InterruptedException 的 catch 代碼塊中調用 interrupt() 中斷線程是基本操作。
自定義標記位
interrupt() 基本上是我們推薦的,唯一的中斷線程的方法。或許有人會問,自己在線程中定義一個中斷標記位不是也能實現線程中斷嘛,像這樣:
class StopThread implements Runnable {private boolean flag = true;public void run() {while (flag) {System.out.println(Thread.currentThread().getName() + "......++++");}}// 外部調用注入方法控制標記位進而停止線程public void setFlag(boolean flag) {this.flag = flag;}
}
一般情況下確實可以,但如果線程中執行的代碼有類似 wait() 這樣的阻塞方法,那么該線程就會進入等待狀態,在線程池中等待喚醒,在沒有其它線程喚醒它的情況下,它就無法通過標記位的方式結束線程:
public synchronized void run() {while (flag) {try {wait();} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + "......++++");}}
在這種情況下,使用 interrupt() 會更好,因為:
- 一般的阻塞方法,如 sleep()、wait() 等本身就支持中斷的檢查
- 檢查中斷標記位和自定義的標志位沒什么區別,用中斷標記位還可以避免聲明自定義的標志位,減少資源的消耗
- interrupt() 方法會將線程從阻塞狀態強制恢復到運行狀態中來,讓線程具備 cpu 的執行資格,但是強制動作會發生 InterruptedException,需要處理
中斷異常是如何被拋出的
我們以 Thread.sleep() 為例,進入源碼看下中斷異常是如何被拋出的:
/*** sleep 期間,線程不會失去已經獲取到的同步鎖。** @throws InterruptedException* 如果任何線程中斷了當前線程,那么會拋出這個異常并且* 清除掉當前線程的中斷狀態。*/public static native void sleep(long millis) throws InterruptedException;
想要查看 sleep() 的 native 源碼,要先在 src/share/native/java/lang/Thread.c 文件中,找到 sleep() 在 JVM 中對應的方法 JVM_Sleep:
#include "jni.h"
#include "jvm.h"#include "java_lang_Thread.h"#define THD "Ljava/lang/Thread;"
#define OBJ "Ljava/lang/Object;"
#define STE "Ljava/lang/StackTraceElement;"
#define STR "Ljava/lang/String;"#define ARRAY_LENGTH(a) (sizeof(a)/sizeof(a[0]))static JNINativeMethod methods[] = {{"start0", "()V", (void *)&JVM_StartThread},{"stop0", "(" OBJ ")V", (void *)&JVM_StopThread},{"isAlive", "()Z", (void *)&JVM_IsThreadAlive},{"suspend0", "()V", (void *)&JVM_SuspendThread},{"resume0", "()V", (void *)&JVM_ResumeThread},{"setPriority0", "(I)V", (void *)&JVM_SetThreadPriority},{"yield", "()V", (void *)&JVM_Yield},{"sleep", "(J)V", (void *)&JVM_Sleep},{"currentThread", "()" THD, (void *)&JVM_CurrentThread},{"countStackFrames", "()I", (void *)&JVM_CountStackFrames},{"interrupt0", "()V", (void *)&JVM_Interrupt},{"isInterrupted", "(Z)Z", (void *)&JVM_IsInterrupted},{"holdsLock", "(" OBJ ")Z", (void *)&JVM_HoldsLock},{"getThreads", "()[" THD, (void *)&JVM_GetAllThreads},{"dumpThreads", "([" THD ")[[" STE, (void *)&JVM_DumpThreads},{"setNativeName", "(" STR ")V", (void *)&JVM_SetNativeThreadName},
};#undef THD
#undef OBJ
#undef STE
#undef STR// 注冊 methods[] 中的 native 方法
JNIEXPORT void JNICALL
Java_java_lang_Thread_registerNatives(JNIEnv *env, jclass cls)
{(*env)->RegisterNatives(env, cls, methods, ARRAY_LENGTH(methods));
}
然后去 jvm.cpp 文件中找到這個方法:
可以看到如果線程的中斷標記位已經為 true,調用 sleep 方法就會拋出 InterruptedException。在拋出 InterruptedException 之前,中斷標記位會被清除為 false。
3、線程的狀態
3.1 狀態定義與狀態轉移
線程狀態也被稱為生命周期,指的是 JVM 中的線程狀態,而不是操作系統的。Thread 中定義的枚舉類 State 規定了線程的 6 種狀態:
- 初始(NEW):新創建了一個線程對象,但還沒有調用 start()。這時僅僅由虛擬機分配內存并初始化變量值。如果此時錯誤地調用了 run(),該線程就不再處于初始狀態,不能再調用 start()。
- 可運行(RUNNABLE):Java 線程中將就緒(ready)和運行中(running)兩種狀態統稱為“可運行”。
線程對象創建后,其他線程(如主線程)調用了該對象的 start() 后會進入就緒狀態,虛擬機會創建方法調用棧和程序計數器。該狀態的線程位于可運行線程池中,等待被線程調度選中,獲取 CPU 的使用權。即線程可以運行,但是尚未運行。
就緒狀態的線程在獲得 CPU 時間片后,開始執行 run() 就變為運行中狀態(running)。 - 阻塞(BLOCKED):表示線程阻塞于鎖。
- 等待(WAITING):進入該狀態的線程需要等待其他線程做出一些特定動作(如通知或中斷)。
- 等待超時(TIMED_WAITING):該狀態不同于 WAITING,它可以在指定的時間后自行返回,也就是計時等待。
- 終止(TERMINATED):表示該線程已經執行完畢,包括 run() 或 call() 執行完成,線程正常結束;線程拋出未捕獲的 Exception 或 Error;直接調用了 stop() 結束線程(容易死鎖,不推薦)。
狀態轉移圖如下所示:
說明:
- 主線是初始->運行->終止,new 創建的新線程要調用 start() 才能進入運行狀態。運行狀態內部又分為運行中(具備執行權)和就緒(具備執行資格但沒有執行權)兩種狀態。注意 Java 中是把運行中和就緒統一視為運行狀態,但是在操作系統的觀點中,認為這兩個狀態是分開的、兩個獨立的狀態。
- 運行狀態的線程可以通過調用 Object.wait() 或 Thread.sleep() 等方法進入等待狀態,方法上加時間參數的會進入等待超時狀態。等待狀態下的線程沒有執行資格,需要通過 notify()、notifyAll() 等方法喚醒。
- 阻塞狀態(具備執行資格但無執行權)只有一種情況,就是等待獲取 synchronized 鎖,拿到鎖之后就變成了運行狀態(阻塞式 IO 方法應該是這種情況?)。注意使用顯式鎖 Lock 等待鎖時,進入的是等待/等待超時狀態,因為其底層使用的是 LockSupport 類實現的。因此系統中能讓線程進入阻塞狀態的有且僅有 synchronized 關鍵字。
- 阻塞狀態是一種被迫的等待(因為拿不到鎖只能等著),而等待狀態是一種主動的等待(主動調用 wait() 或 sleep())。
此外還有幾點注意事項:
- 主線程結束并不會影響其它線程。
- isAlive() 可以測試某個線程是否存活,就緒、運行、阻塞狀態返回 true,新建、死亡返回 false。
- 不要對已經死亡的線程調用 start(),也不要對新建的線程調用兩次 start(),否則會引發 ILLegalThreadStateException。
針對以上情況,當發生如下特定情況時可以解除上面的阻塞,使線程重新進入就緒狀態:
3.2 涉及的方法介紹
join()
可以通過 join() 控制線程的執行順序,哪個線程執行到了 join() 就釋放執行權并凍結在線程池中,等調用了 join() 的線程執行完后,才恢復可執行狀態,與其它線程爭奪執行權。比如說:
public static void main(String[] args) throws Exception{Demo d = new Demo();Thread t1 = new Thread(d);Thread t2 = new Thread(d);t1.start();t2.start();t1.join();//t1線程要申請加入進來,運行。臨時加入一個線程運算時可以使用join方法。for(int x=0; x<50; x++){System.out.println(Thread.currentThread()+"....."+x);}}
}
主線程開啟了 t1、t2 兩個線程,在執行 t1.join() 之前,是三個線程在輪番運行的。在主線程中執行了 t1.join() 后,主線程釋放執行權,凍結在線程池。等待 t1 執行完畢后,恢復可執行狀態,與 t2 爭奪執行資格,輪番運行。
如果在 A 線程中調用了 B 線程的 join() 方法,那么 A 線程將被阻塞直到 B 線程執行完。它有三種重載形式:
第三種形式很少被用到,因為程序、操作系統和計算機硬件都無法精確到納秒。
sleep()
sleep() 用于線程睡眠,調用該方法可以讓線程暫停一段時間進入等待狀態,它有兩種重載形式:
同樣是因為程序、操作系統和硬件設備無法精確到納秒,因此第二個方法很少被使用。
處于睡眠時間內的線程不會獲得執行的機會,即使系統中沒有其它可執行的線程,處于 sleep() 中的線程也不會執行;已經獲得鎖的線程如果執行了 sleep(),只會釋放執行權,但不會釋放鎖。
yield()
yield() 用于線程讓步,它也可以讓當前正在運行的線程暫停,但它不會使線程進入等待狀態,只是將該線程轉入可運行狀態,然后讓系統的線程調度器重新調度一次。完全可能的情況是:某個線程調用了 yield() 暫停之后,調度器又將其調度出來重新執行。
實際上,當 A 線程調用了 yield() 之后,只有優先級大于等于 A 的處于可運行狀態的線程才會獲得執行機會。
暫停當前正在執行的線程對象,釋放執行權,然后讓包括自己在內的所有線程再次爭奪執行權。這樣做會更和諧,不會一直在執行同一個線程。
sleep() 與 yield() 的區別:
其它方法
Object.wait() 與 Thread.sleep() 的對比:
- wait() 可以指定時間也可以不指定,sleep() 必須指定時間。
- 在同步中時,對 cpu 的執行權和鎖的處理不同。wait() 釋放執行權,釋放鎖;sleep() 釋放執行權,不釋放鎖。
class Demo {void show() {synchronized(this) {wait();//t0 t1 t2}}void method() {synchronized(this)//t4{notifyAll();}//t4}
}
同步中誰有鎖誰執行,如果 t0 t1 t2 都卡在 wait() 并被 t4 喚醒,雖然 3 個都活了,但是只有一個能持鎖執行代碼。
此外,我們經常會調用 Thread 的 toString() 輸出線程信息,線程的字符串表現形式,包括線程名稱、優先級和線程組:
public String toString() {ThreadGroup group = getThreadGroup();if (group != null) {return "Thread[" + getName() + "," + getPriority() + "," +group.getName() + "]";} else {return "Thread[" + getName() + "," + getPriority() + "," +"" + "]";}}
4、線程屬性
4.1 優先級
每個 Java 線程都有一個優先級,可以通過 Thread.setPriority() 將線程優先級設置在 MIN_PRIORITY(數值為1)和 MAX_PRIORITY(數值為10)之間,默認優先級為 MIN_PRIORITY(數值為5)。
線程調度器會優先選擇優先級高的線程來運行。但是線程優先級是高度依賴于系統的。當 JVM 依賴于宿主機平臺的線程實現機制時,Java 線程的優先級會先被映射到宿主機平臺的優先級上。例如 Windows 有 7 個優先級,而在 Oracle 為 Linux 提供的 JVM 中,線程的優先級被忽略——所有線程具有相同的優先級。
如果確實要使用優先級,需要注意,如果高優先級的線程沒有進入非活動狀態(阻塞或等待),低優先級的線程可能永遠也得不到執行,發生線程饑餓的情況(線程饑餓就是指低優先級的線程,總是拿不到執行時間)。
4.2 守護線程
守護線程也稱為后臺線程、精靈線程,它是在后臺運行的線程,任務是為其它線程提供服務,JVM 的垃圾回收線程就是典型的守護線程。此外,守護線程也可用于發送計時信號或清空過時的高速緩存。
可以使用 Thread 的 setDeamon(true) 將一個線程設置為守護線程(但是必須在該線程啟動之前,否則會引發 ILLegalThreadStateException),isDeamon() 用來判斷是否是守護線程。
主線程默認是前臺線程,但不是所有線程默認都是前臺線程,規則是:前臺線程創建的子線程默認是前臺線程,守護線程創建的線程默認是后臺線程。
守護線程也會去爭搶同步鎖。
守護線程的特征為當所有前臺線程死亡后,虛擬機會退出并通知后臺線程死亡。假如在主線程中開啟了一個執行耗時操作的守護線程,那么很有可能守護線程的任務并不會執行完,因為主線程不會等待守護線程,只要主線程跑完了,守護線程也會自動消亡。但如果在主線程中啟動一個非守護線程,那么主線程會等待該子線程執行完任務。
永遠不要讓守護線程去訪問文件、數據庫這樣的固有資源,因為他會在任何時候甚至在一個操作的中間被中斷。
4.3 線程組與未處理的異常
Java 允許程序直接對 ThreadGroup 進行控制。如果沒有顯式指定一個線程屬于哪個線程組,那么它就屬于默認線程組,即與創建它的線程在同一線程組。線程中途不能改變它所屬的線程組。
以下構造方法用來指定新創建的線程屬于哪個線程組:
以上方法也可以指定線程組的名字,這個名字也不能中途更改。
常用的操作線程組的方法:
ThreadGroup 實現了一個接口 Thread.UncaughtExceptionHandler,該接口用于處理未捕獲的異常。
線程的 run() 不能拋出任何受查異常(刨去非受查異常剩余的異常),而非受查異常(所有派生于 Error 或 RuntimeException 的異常)會導致線程終止。
線程因為異常終止之前,會將異常傳遞到未捕獲異常的處理器中,該處理器必須實現 Thread.UncaughtExceptionHandler 接口,并且通過 Thread 的 setUncaughtExceptionHandler() 為某一個線程設置處理器,或者用靜態的 setDefaultUncaughtExceptionHandler() 為所有線程設置一個默認的處理器。如果沒有調用以上方法給線程設置處理器,那么線程的處理器就是該線程的 ThreadGroup 對象。
UncaughtExceptionHandler 接口的唯一方法 uncaughtException() 會按照如下優先順序操作:
- 如果該線程組有父線程組,則調用父線程組的 uncaughtException()
- 如果 Thread 的 getDefaultUncaughtExceptionHandler() 返回一個非空處理器,則調用該處理器
- 如果 Throwable 是 ThreadDeath 的一個實例,就什么都不做,否則,就將線程名字以及 Throwable 的棧軌跡輸出到 System.err 上。
其中,最后一點的棧軌跡就是應用發生崩潰時我們看到的調用棧信息。