Overview of Content
在現今高效能運算的時代,了解 Thread 和 Process 的運作機制成為每個軟體開發者的必備技能。本篇文章將帶你全面掌握 Java 多執行緒技術,深入解析從基礎概念到高級應用的每個重要環節
首先,我們將探討 Thread 與 Process 的基礎概念,了解軟、硬體 Process 和多執行緒技術的基本原理。接著,我們會比較併發與並行的差異,分析多執行緒的優缺點,幫助你更好地選擇適合的開發模式
隨後,我們會深入 Java Thread 的觀念,包括 Thread 的狀態圖和生命週期的結束過程,讓你清楚了解每個 Thread 的生命歷程。接著,我們將討論各種創建和運行任務的方法,從 Thread
、Runable
到 Callable
,並比較說明 run 和 start 的差異。
在排程模型部分,你將學會如何調整 Thread 的優先序、插入執行緒及執行序讓出的操作,確保任務的高效調度。對於守護執行序(Daemon
)和執行序中斷的處理,我們也將提供詳細的實踐指導,確保你能應對各種複雜的執行緒操作。
最後,我們將介紹一些其他重要的執行緒相關知識,如 Timer
計時器、執行序群組(ThreadGroup
)以及未捕獲的異常處理,讓你全面提升多執行緒編程能力。
以下可能會混用 “線程”、“執行序”,兩者是相同意思
寫文章分享不易,如有引用參考請詳註出處,如有指導、意見歡迎留言(如果覺得寫得好也請給我一些支持),感謝 😀
個人程式分享時比較注重「縮排」,所以可能不適合手機的排版閱讀,建議切換至「電腦版」、「平板版」視窗看
Thread & Process 基礎概念
認識軟、硬體 Process
● Process 這個詞可以使用在「硬體」、「軟體」兩個領域內,而兩種是不同的概念
● 「硬體」的 Process 概念:
● 「軟體」的 Process 概念:
軟體的 Process 也就是行程(或稱之為進程),這是系統規劃「每個應用的最小單位」,並且每個 Process 都會有一個完整個虛擬記憶體空間
而這個 Process 的資料(記憶體、暫存器等等)則是由核心空間做管理… 概念圖如下
認識 Thread 多執行序
Process 算是系統對於 CPU 特性應用的規劃,將每個應用抽象為「Process」來管理
● 多執行序(也稱為多線程):
Simultaneous Multithreading
(SMT) 可複製處理器上的結構狀態,「共享處理器的資源」,執行緒之間進行切換 由於時間間隔很小,來給用戶造成一種多個執行緒同時執行的 假象
也就是其實單核心執行緒也可以達到 Multithreading 的使用,它就是透過快速切換來達成同時執行的假象
● 快速切換的手法 時間片輪轉機制(RR 調度)
時間片輪轉機制就是設定一個固定的 CPU 時間,當時間一到就進行中斷處理,切換 CPU 資源給另一個執行緒
● 如果在時間片結束時執行序還在運行, 則會暫停該執行緒,並將 CPU 分配給另一個執行序
● 如果執行序在時間片結束前阻塞,則 CPU當即進行切換
這種時間片輪詢機制是由內核(軟體)安排的機制,並非 CPU 自身機制(也就說是系統在利用 CPU 資源)
● 執行緒上下文切換
Context switch
由於 CPU 並不會紀錄 Thread 的相關資訊(依照 CPU 的特性,它也不應該拿來紀錄 Thread 相關資訊),這些資訊應該由內核紀錄
而切換 Thread 時就必須紀錄、載入資訊,這些資訊稱為 執行緒上下文
切換要做的事情 : 保存和裝入暫存器、內存映像,更新各種表格、隊列
Thread & Process 的差異:併發 & 並行概念
● Process 進程:(這裡談論的是軟體 Process)
應用資源的最小單位,每個 Process 都擁有獨立的虛擬記憶體,這些虛擬記憶體之間無法相互訪問、交換資源(CPU、暫存器空間、磁盤... 等等)
它是一個獨立單位,並不會被其他進程影響,並且資源不共享(如果需要共享則需要利用 Socket、內存共享、文件、Binder… 等等)
● Thread 執行序 :
處理器(CPU
) 調度的最小單位;它的特性是可以共享內存地址,這也就意味著資源共享;擁有一些私有資源(暫存器(局部變量)、堆棧)
它的好處是足夠快速,操作相對於進程通訊來說也較為方便簡單;但是資源的爭奪不同步又是另一個大問題
● 另外,我們在軟體開發中會以「並行」、「併發」兩個詞彙來描述 Process、Thread 的兩種特性;簡單的理解就是,並行資源不共享(Process
),併發資源共享(Thread
)
● 並行(Process):它能真正意義上的做到同時執行,可並排處理不同事務,概念圖如下
● 併發(Thread): 談論併發時一定要 加上時間的限制 (單位時間的併發量);這是內核機制所做出的功能,它可以增加 CPU 的吞吐量(如果運用得當的話)
● 離開了時間單位的話談論併發是沒有意義的
實現併發技術相當複雜,最容易理解的就是時間輪轉機制,以快速切換來達成同時處理的假象
Thread 優缺點
● 使用 Thread 的優點:
A. 充分利用 CPU 資源(當然,必須要有適當的規劃,否則任意使用 Thread 也會導致效能不佳)
B. 加快了用戶的響應時間,在用戶使用當前資源時,在後端同時間加載其它資源
C. 讓代碼 模塊、異步、簡單化 : 可獨立化一個代碼區塊,方便日後維護
● 雖然 Thread 很方便,但它仍有需要注意的使用點,而使用 Thread 的注意事項如下:
A. 線程不安全:
由於共享資源,在寫入操作時會有同步問題(讀取不會有問題),處理不好也會影響效能
B. 線程死鎖:
如果有兩把鎖,要共同取得才能操作的話,不同線程持有不同鎖,而且都不釋放
C. 線程過多:
線程切換需要時間,如果過多線程會造成過度切換,造成死機 (可用線程池解決)
Java Thread 觀念
Thread 類是 Java 對執行序概念的抽象
Thread 狀態圖
● 了解 Thread 的狀態相當重要(可以之後再來反覆查看),Java 的每個方法操作都會觸發 Thread 處於不同狀態,不同狀態下的 Thread 又會有不同特性
Thread 狀態 | 觸發該狀態的函數 | 補充 |
---|---|---|
新建(New ) | 創建 Thread 物件 | 目前仍運行在 |
就緒(Runnable ) | run 、start | 這兩個函數的差異,後續會再提及;主要到該階段,Thread 就可以調用處理任務 |
休眠(Blocking ) | sleep 、yield | CPU 休眠(不耗費 CPU 時間) |
等待(Blocking ) | Object#wait | CPU 休眠(不耗費 CPU 時間),通常用於 Thread 通訊作用 |
執行(Running ) | join | 將當前任務插入到指定 Thread 之前運行 |
● Thread#
sleep
& Object#wait
並不耗費 CPU 時間
Thread 生命週期結束
● Thread 生命週期結束就是 Thread 結束生命週期,而生命週期結束一般來說有兩種方式 1. 正常結束、2. 異常結束
● 正常結束
可以使用 Thread#isAlive
方法判斷 Thread 是否以經結束生命週期
public class UseThread extends Thread {
public static void main(String[] args) throws InterruptedException {
Thread t = new UseThread();
t.start();
Thread.sleep(1000);
System.out.println("Is thread alive=" + t.isAlive());
}
}
● 異常結束
當 Thread 運行時(我們可稱之為 WorkThread)發生異常並不會影響主執行緒,主執行緒仍可正常執行
public class UseThread extends Thread {
@Override
public void run() {
throw new RuntimeException(Thread.currentThread().getName() + " occur exception");
}
public static void main(String[] args) throws InterruptedException {
Thread t = new UseThread();
t.start();
Thread.sleep(1000);
System.out.println("Is thread alive=" + t.isAlive());
}
}
從下圖,我們也可以看到 WorkThread 發生異常時,會拋出異常並結束(alive=false
),但是並不會影響主執行緒的運行
Thread 任務創建、運行
執行緒它需要去執行一個「任務」,而 Java 對於執行緒的任務創建有三種基礎的方式,相關類有 1. Thread
(class)、2. Runnable
(interface)、3. Callable
(generic interface)
Thread 運行的任務大多都是耗時任務,像是 IO 處理,網路請求... 等等
Thread 創建任務
● Thread 類(Android Thread API):
Thread 本身就是 Java 對執行緒的抽象,而它本身內部就會帶有一個任務函數 run()
… 我們可以透過 繼承 Thread 並複寫 run
方法 並在內部撰寫一些耗時任務
class ExtendThread extends Thread {
@Override
public void run() {
System.out.println("Task running");
}
public static void main(String[] args) {
ExtendThread t = new ExtendThread();
t.start();
}
}
● 建立完 Thread 物件後,就開始執行了嗎?
new Thread()
可以建立一個 Thread 實例,但並未真正的跟執行序產生關係,在執行start()
方法後才真正的跟執行緒產生關係
Runable 創建任務
● Runnable 介面 (Android Runnable API):
Runable 是一種介面(interface
),既然是介面就可以實作(implememts
)或是創建匿名類,並且在創建出來後,須將其賦予 Thread
Thread 相較於 Runnable 來說,更加的「輕量級」,這個原因是因為 Java 的單繼承特性導致;由於單繼承會影響到類的繼承只能選擇一個,而介面(interfcae
)不同,一個類可以實作多個介面,這才導致我們覺得 Runnable 更加簡便
A. 類實作 Runnable 介面,創建 Thread 的任務
class RunUsage implements Runnable {
@Override
public void run() {
// Do something
}
public static void main(String[] args) {
Thread t = new Thread(new RunUsage());
t.start();
}
}
B. 使用匿名類實作 Runnable 介面,同樣可以創建 Thread 任務
class AnonymousRun {
private Runnable anon = new Runnable() {
@Override
public void run() {
// Do something
}
};
public static void main(String[] args) {
Thread t2 = new Thread(new AnonymousRun().anon);
t2.start();
}
}
Callable 創建任務
● Callable 介面(Callable API):
● Callable 是一個泛型介面(generic interface
)我們無法直接使用 Thread 來運行 Callable 創建的任務,它必須透過 FutureTask 類來執行
● FutureTask 類(泛型類):
我們同樣可以把 FutureTask 類當成是 Java 抽象化執行緒概念的類,但它比起 Thread 類還要更佳的有可控性也提供了更多的方法,常用的方法如下表
方法 | 描述 |
---|---|
FutureTask<V>(Callable<V> callable) | 構造一個 FutureTask,它將會執行給定的 Callable |
FutureTask<V>(Runnable runnable, V result) | 構造一個 FutureTask,它將會執行給定的 Runnable,並且在運行結束時返回指定的結果 |
boolean cancel(boolean mayInterruptIfRunning) | 嘗試取消任務的執行,如果任務已經完成或已經被取消,則無法取消任務 |
V get() | 等待任務完成並返回計算結果。如果任務被取消或者拋出異常,則會拋出相應的異常 |
V get(long timeout, TimeUnit unit) | 等待任務完成並在指定的超時時間內返回計算結果。如果超時、任務被取消或者拋出異常,則會拋出相應的異常 |
boolean isCancelled() | 如果任務在正常完成之前被取消,則返回 true |
boolean isDone() | 如果任務已完成(正常完成、取消或拋出異常),則返回 true |
void run() | 執行任務。如果任務已經完成或者已經被取消,則不會再次執行 |
FutureTask 是透過實作 Future 介面來達到異步任務的空士(可取得結果 or 取消);FutureTask 與 Future、Callback、Runnable 的 UML 如下所示
class CallableUsage implements Callable<String> {
@Override
public String call() throws Exception {
// Do something
return "Hello World";
}
public static void main(String[] args) throws ExecutionException, InterruptedException, TimeoutException {
Callable<String> callable = new CallableUsage();
FutureTask<String> futureTask = new FutureTask<>(callable) {
@Override
protected void done() {
System.out.println("Task done.");
}
};
futureTask.run();
String result = futureTask.get(3, TimeUnit.SECONDS);
System.out.println("Get result: " + result);
}
}
Thread 運行任務:run、start 差異
● Thread 類中有 run
、start
兩個方法,而這兩個方法看似都可以執行任務,但是它們是有很大的差異的,start
方法是真正讓新執行緒去執行任務,而 run
方法則是讓當前的執行緒去執行任務,兩個方法的概念圖如下
run()
是順序執行(無心執行序運行);start()
是同時執行(有執行序運行)
● 我們來比較一下兩者個實作差異:
● Thread# run()
方法:
業務邏輯實現的地方,也就是執行緒要做的事情,通常是一些耗時算法,也就是我們上面小節說的「任務」) ,從以下源碼中我們可以看到 run 就如同一般的方法,沒有切換執行緒的動作
並且 run 方法可以重複執行
// Thread.java
public class Thread implements Runnable {
private Runnable target;
public Thread(Runnable target) {
this(null, target, "Thread-" + nextThreadNum(), 0);
}
public Thread(ThreadGroup group, Runnable target, String name,
long stackSize) {
this(group, target, name, stackSize, null, true);
}
private Thread(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
boolean inheritThreadLocals) {
....
this.target = target;
}
@Override
public void run() {
if (target != null) {
target.run();
}
}
}
哪個 Thread 呼叫
run()
方法,該方法就在哪個 Thread 運行
● Thread# start()
方法:真正讓新建的執行序運行任務
start()
方法會讓一個 Thread 的 狀態轉為就緒 接著等待 CPU 分配(真正被調用的時機不確定,這由系統決定如何分配),分配到後才會由新的執行序調用 Thread 的 run()
方法,也就是這時運行 run()
方法的執行序不在是之前的執行序,而是新的執行序
在 start()
前一直都是使用過往的執行緒 (呼叫的舊執行緒),真正意義創造新執行緒是在 nativeCreate
本地方法中
// Thread.java
public class Thread implements Runnable {
...
public synchronized void start() {
...
group.add(this);
started = false;
try {
// 呼叫 Native 方法,創建新 thread
nativeCreate(this, stackSize, daemon);
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
/* do nothing. If start0 threw a Throwable then
it will be passed up the call stack */
}
}
}
}
每個版本的 JDK 對於 Thread#start 方法的實現都有些微不同
● 在使用
Thread
類時要注意幾件事:A. 不要隨意 Override
start
方法!B.
start
方法不可重複呼叫,否則會 拋出IllegalThreadStateException
異常
Thread 排程:排程模型
JVM 的其中一個任務就是負責執行序(Thread
)的排程;而我們常見的排程模型有兩種
A. 分時式排程模型
每個執行緒擁有相同(公平)的 CPU 時間片,用來平均佔用 CPU 時間片
B. 搶佔式排程模型
Java 採用的排程模式;該模式會依照 1. 執行緒的優先順序進行排程,如果相同則隨機、2. 執行序會一直執行直到,無法執行!
● 執行序無法繼續執行的原因
● JVM 控制暫時放棄(包含主動、被動),那執行序會轉為 就緒狀態
● 執行序進入 Blocking 狀態(可能在等待協作)
● 執行序自然結束
● 這也與平台有相關
由於執行緒不是跨平台的,所以 Thread 的順序並非只取決於 JVM,同時也會依賴作業系統
調整 Thread 優先序:priority
● 當多個線程處理就緒(Runnable
)狀態,那 JVM 會先依照 Thread 的優先序進行線程的排序!
Thread 優先序選擇 | 概述 |
---|---|
MAX_PRIORITY | 最高優先 |
MIN_PRIORITY | 最低優先 |
NORM_PRIORITY | 普通優先 |
● 由於個 作業系統平台對於 Thread 的優先序不同,所以 JVM 有時候不能很好的映射,所以建議都使用 JVM 提供的
MAX_PRIORITY
、MIN_PRIORITY
、NORM_PRIORITY
設置像是 Window 只有
7
個順序,而 Sun 公司的作業系統 Solaris 則有231
個優先序可以控制
A. 優先度設置
class PriorityThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + ": " + i);
}
}
public static void main(String[] args) throws InterruptedException {
PriorityThread t1 = new PriorityThread();
t1.setPriority(Thread.MAX_PRIORITY);
t1.setName("AAA");
PriorityThread t2 = new PriorityThread();
t2.setPriority(Thread.MIN_PRIORITY);
t2.setName("BBB");
t1.start();
t2.start();
Thread.sleep(300);
}
}
從結果可見,優先序確實會影響執行序執行
B. 無優先度設置(把 setPriority
Mark 起來即可)
插入執行緒:join
● Thread 的 join 方法,可以將目前執行中的執行序轉到掛起狀態(Blocking
),直到另一個執行序結束,它才會恢復(Running
)
class JoinUsage {
public static void main(String[] args) {
TestJoin j1 = new TestJoin("Alien");
TestJoin j2 = new TestJoin("Pan");
TestJoin j3 = new TestJoin("Kyle");
j1.start();
// 1 :
try {
j1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
j2.start();
// 2 :
j3.start();
}
}
class TestJoin extends Thread {
TestJoin(String name) {
super(name);
}
@Override
public void run() {
for(int i = 0; i < 10; i++) {
System.out.println(this.getName() + ": " + i);
}
}
}
● 當未使用 Join 方法時,有三個執行序就會互相搶執行順序,這樣的問題會造成無法有效的使用執行序的效率來達成共同任務
A. 在呼叫 start()
後立刻呼叫 join
:
當前執行的執行序就會掛起(Blocking
)強制先執行完該加入的執行序的任務,再繼續其他任務
以目前範例來講,就是 MainThread 掛起,先運行
j1
B. 在呼叫 start()
後沒有馬上呼叫 join
:
首先,執行緒會跟呼叫 join()
之前的執行序互搶 CPU 資源,在呼叫 join
之後,正在執行的執行序就會掛起直到目標執行緒執行完任務
執行序讓出:yield
● 使用 Thread
#yield
函數可以讓當前執行序主動讓出 CPU 執行時間(讓出 CPU 使用的時間),JVM 就會讓所有未掛起的執行序來搶奪 CPU 資源
● 讓出後,哪個執行序會搶到 CPU 資源?
這不一定!可能由讓出的執行序重新獲得,或是由其他的執行序獲得
--實做--
class YieldUsage {
public static void main(String[] args) {
TestNormal j2 = new TestNormal("Normal");
TestYield j1 = new TestYield("Yield");
j1.start();
j2.start();
}
}
class TestYield extends Thread {
TestYield(String name) {
super(name);
}
@Override
public void run() {
for(int i = 0; i < 10; i++) {
System.out.println(this.getName() + ": " + i);
if(i == 5) {
Thread.yield();
}
}
}
}
class TestNormal extends Thread {
TestNormal(String name) {
super(name);
}
@Override
public void run() {
for(int i = 0; i < 10; i++) {
System.out.println(this.getName() + ": " + i);
}
}
}
TestYield 在數值為 5 的時候讓出了資源,TestNormal 執行序搶到,但也 有可能 TestYield 的線程自己再次搶到(下圖就是)
● 執行序的
Yield
、優先度:
Yield
方法只會將 CPU 執行權力讓給 同優先級別、更高優先級別的執行序(不會讓給低優先度的執行序)class YieldUsage { public static void main(String[] args) { TestNormal j2 = new TestNormal("Normal"); TestYield j1 = new TestYield("Yield"); j2.setPriority(Thread.MIN_PRIORITY); j1.start(); j2.start(); } } class TestYield extends Thread { TestYield(String name) { super(name); } @Override public void run() { for(int i = 0; i < 10; i++) { System.out.println(this.getName() + ": " + i); // 雖然讓出了 CPU 資源,不過另一個執行序的優先度太低, // 仍舊是自身先執行 if(i > 5) { Thread.yield(); } } } } class TestNormal extends Thread { TestNormal(String name) { super(name); } @Override public void run() { for(int i = 0; i < 10; i++) { System.out.println(this.getName() + ": " + i); } } }
相對來說
sleep
方法就公平分配,低優先度仍可搶奪 CPU 資源
守護執行序:Daemon
● 守護執行序:(也稱之為背景執行序)
背景執行序的特點在於,背景執行緒會與前景執行序生命週期相伴! 只有所有前景執行序都結束後背景執行序才會結束
JVM 的 GC 就是典型的背景執行序
A. Thread#setDaemon
設定為 false
(預設值):也就是非守護執行序,只是一般的執行序
class DaemonUsage {
public static void main(String[] args) {
System.out.println(Thread.currentThread().getName() + "--- start");
TestDaemon d = new TestDaemon();
d.setDaemon(false);
d.start();
System.out.println(Thread.currentThread().getName() + "--- finish");
}
}
class TestDaemon extends Thread {
@Override
public void run() {
for(int i = 0; i < 10; i++) {
System.out.println(this.getName() + ": " + i);
}
}
}
從結果看來,非背景執行序,不會與前景(Main Thread
)生命週期相伴,前景執行序會先結束,接著背景執行序繼續執行,直到背景執行序也執行結束才會結束整個應用的生命週期
必須在執行緒啟動前(
start
之前)設置setDaemon
才有用,否則會拋出異常
B. Thread#setDaemon
設定為 true
(設定為守護執行序):以下讓主執行序創建另一個執行序(WorkThread
),並將 WorkThread 設定為守護執行序
可以想像為,讓 WorkThread 守護主執行序,主執行序結束 WorkThread 就一起結束(以主執行序生命週期為主)
class DaemonUsage {
public static void main(String[] args) {
System.out.println(Thread.currentThread().getName() + "--- start");
TestDaemon d = new TestDaemon();
d.setDaemon(true);
d.start();
System.out.println(Thread.currentThread().getName() + "--- finish");
}
}
class TestDaemon extends Thread {
@Override
public void run() {
for(int i = 0; i < 10; i++) {
System.out.println(this.getName() + ": " + i);
}
}
}
從結果來看,背景執行序根本沒有執行的機會,前景(MainThread)結束,背景就會結束
處理執行序中斷
以往 Thread 有一些方法可以處理執行序中斷,像是 suspend
、resume
、stop
…等等,但這些方法都被 Deprecated
了
● 這些 API 被棄用的原因
如果
suspend
一個持有「鎖」的執行序,會導致它在resume
之前都無法釋放鎖,可能導致死鎖
suspend
在暫停執行序(Thread)時不會釋放鎖「鎖」請參考另一篇文章 Java 多執行序 - 同步、鎖
正確處理 interrupt 中斷
● 現在建議的方法是 使用 interrupt()
,拋出個訊號,讓使用者自行斷定是否該停止
● 中斷信號!
●
interrupt()
訊號如果 被InterruptedExceptionThread
、interrupted
抓住後就會清除中斷訊號● 如果使用
isInterrupted()
中斷訊號則不會被清除
● 取得中斷訊號,但不清除
class InterruptedSignal extends Thread {
@Override
public void run() {
for(int i = 0; i < 100; i++) {
System.out.println("i: " + i);
if(this.isInterrupted()) {
System.out.println("isInterrupted Get signal");
// 取得中斷訊號,但不清除
} else {
System.out.println("Working");
}
}
}
public static void main(String[] args) {
InterruptedSignal t = new InterruptedSignal();
t.start();
try {
TimeUnit.MILLISECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
t.interrupt();
}
}
● 取得中斷訊號,並消除中斷訊號
● 使用 Thread#interrupted
這個靜態方法消除中斷訊號
class InterruptedSignalConsume extends Thread {
@Override
public void run() {
for(int i = 0; i < 100; i++) {
System.out.println("i: " + i);
if(Thread.interrupted()) {
System.out.println("Thread.interrupted() get signal");
// 取得中斷信號後,信號就會被清除
} else {
System.out.println("Working");
}
}
}
public static void main(String[] args) {
InterruptedSignalConsume t = new InterruptedSignalConsume();
t.start();
try {
TimeUnit.MILLISECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
t.interrupt();
}
}
● 使用 try/catch 捕捉 InterruptedException
異常!(這種捕捉動作同時會消除中斷訊號~)
class InterruptedSignalConsumeByCatch extends Thread {
@Override
public void run() {
for(int i = 0; i < 100; i++) {
System.out.println("i: " + i);
try {
sleep(1);
} catch (InterruptedException e) {
// 取得中斷信號後,信號就會被清除
System.out.println("Get InterruptedException");
}
}
}
public static void main(String[] args) {
InterruptedSignalConsumeByCatch t = new InterruptedSignalConsumeByCatch();
t.start();
try {
TimeUnit.MILLISECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
t.interrupt();
}
}
其他執行序相關知識
Timer 計時器
● 首先 Timer 這個類本身 並非執行序(Thread)的衍生類!它是 Runnable
的實作者
public abstract class TimerTask implements Runnable {
...
}
● Timer 的使用特色
A. 它會一直運作:直到呼叫 cancel
手動將其關閉,或是發生異常
B. 可以設定是否是守護(背景)執行序
public class TimerUsage extends Thread {
// 設定為非守護執行序
private final Timer timer = new Timer("My Timer", false);
@Override
public void run() {
System.out.println("Thread start: " + Thread.currentThread().getName());
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("Start timer: " + Thread.currentThread().getName());
for (int i = 0; i < 5; i++) {
System.out.println("value: " + i);
}
timer.cancel();
System.out.println("Cancel timer: " + Thread.currentThread().getName());
}
}, 0);
System.out.println("Thread finish: " + Thread.currentThread().getName());
}
public static void main(String[] args) {
System.out.println("Main start.");
new TimerUsage().start();
System.out.println("Main done.");
}
}
● 如果沒有 Cancel 則會一直阻塞住 Blocking
C. Timer 在啟動後會自己創建一個執行序,不用你手動創建
public static void main(String[] args) {
System.out.println("Main start.");
Timer localTimer = new Timer(false);
localTimer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("Start timer: " + Thread.currentThread().getName());
for (int i = 0; i < 5; i++) {
System.out.println("value: " + i);
}
localTimer.cancel();
}
}, 0);
System.out.println("Main done.");
}
D. 可以循環(週期)運作
class TimerUsageCycle extends Thread {
private final Timer timer = new Timer("My Timer Cycle", false);
@Override
public void run() {
System.out.println("Thread start: " + Thread.currentThread().getName());
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("Start timer: " + Thread.currentThread().getName());
for (int i = 0; i < 5; i++) {
System.out.println("value: " + i);
}
}
}, 0, 333);
System.out.println("Thread finish: " + Thread.currentThread().getName());
}
public static void main(String[] args) {
System.out.println("Main start.");
new TimerUsageCycle().start();
System.out.println("Main done.");
}
}
執行序群組:ThreadGroup
● ThreadGroup 可以用來「管理」一系列「存活」的執行序,如果該執行序已經執行完(生命週期結束),那就會被從群組中移除
● JVM 執行應用時會建立一個名為「
main
」的執行群組!在預設行況下,所有執行序都屬於「main」群組
public class ThreadGroupUsage extends Thread {
private final boolean sleep;
ThreadGroupUsage(ThreadGroup group, String name, boolean sleep) {
super(group, name);
this.sleep = sleep;
}
@Override
public void run() {
if (!sleep) {
return;
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// do nothing
}
}
public static void main(String[] args) {
ThreadGroup threadGroup = new ThreadGroup("My Thread Group");
for (int i = 0; i < 5; i++) {
new ThreadGroupUsage(threadGroup, "hello_thread_" + i, i % 2 == 0).start();
}
int activeCount = threadGroup.activeCount();
System.out.println("Active Count: " + activeCount);
Thread[] threadArray = new Thread[activeCount];
threadGroup.enumerate(threadArray);
for (int i = 0; i < activeCount; i++) {
System.out.println(threadArray[i].getName() + " is alive");
}
}
}
從結果可以看出死亡的執行序並不會包含在 Thread 中
執行序未捕獲的異常
● 從 JDK 1.5 版本開始,有加強對執行序的例外處理;如果執行序沒有捕獲例外,JVM 會尋找應用中的 UncaughtExceptionHandler
實體並對其他發送異常
● 而 UncaughtExceptionHandler
的使用是需要註冊的,並有處理順序,未捕獲異常的順序如下
A. Thread 自身的 UncaughtExceptionHandler
(優先度最高,處理過後不會再傳入 ThreadGroup
& Default
)
Thread t1 = new Thread();
UncaughtUsage uncaughtUsage = new Thread.UncaughtExceptionHandler() {
@Override
public void uncaughtException(Thread t, Throwable e) {
...
}
}
t1.setUncaughtExceptionHandler(uncaughtUsage);
B. 如果該執行序有群組,則執行群組的 UncaughtExceptionHandler
new ThreadGroup {
@Override
public void uncaughtException(Thread t, Throwable e) {
super.uncaughtException(t, e);
}
}
C. 預設的 UncaughtExceptionHandler
(也就是靜態 Thread#setDefaultUncaughtExceptionHandler 函數設定)
UncaughtUsage uncaughtUsage = new Thread.UncaughtExceptionHandler() {
@Override
public void uncaughtException(Thread t, Throwable e) {
...
}
}
Thread.setDefaultUncaughtExceptionHandler(uncaughtUsage);
D. 都沒有捕捉,則往 System.err
的標準輸出(優先度最低)
● 使用範例:
public class UncaughtUsage implements Thread.UncaughtExceptionHandler {
private final String name;
UncaughtUsage(String name) {
this.name = name;
}
@Override
public void uncaughtException(Thread t, Throwable e) {
System.out.println(name +", Get uncaught exception: " + e.getMessage());
}
}
class MyThreadGroup extends ThreadGroup {
public MyThreadGroup() {
super("My_Thread_Group");
}
@Override
public void uncaughtException(Thread t, Throwable e) {
System.out.println(getName() + " get uncaught exception.");
super.uncaughtException(t, e);
}
}
class TestThread extends Thread {
TestThread(ThreadGroup group, String name) {
super(group, name);
}
@Override
public void run() {
System.out.println(getName() + ", Ready throw exception.");
throw new RuntimeException(getName() + "~ Hello exception.");
}
}
A. 使用預設捕捉
public static void main(String[] args) {
// 使用預設捕捉
UncaughtUsage defaultUncaught = new UncaughtUsage("Default Uncaught");
Thread.setDefaultUncaughtExceptionHandler(defaultUncaught);
// 不設定 Group
Thread t1 = new TestThread(null, "Thread-1(Use specific uncaught)");
Thread t2 = new TestThread(null, "Thread-2");
t1.start();
t2.start();
}
B. 使用設定的預設捕捉 & ThreadGroup 的捕捉
public static void main(String[] args) {
// 預設捕捉
UncaughtUsage defaultUncaught = new UncaughtUsage("Default Uncaught");
Thread.setDefaultUncaughtExceptionHandler(defaultUncaught);
// 創建 Group
MyThreadGroup threadGroup = new MyThreadGroup();
// 設定 Group
Thread t1 = new TestThread(threadGroup, "Thread-1(Use specific uncaught)");
Thread t2 = new TestThread(threadGroup, "Thread-2");
t1.start();
t2.start();
}
● 兩個
uncaughtException
函數都會被呼叫
C. 使用設定的預設捕捉 & ThreadGroup 的捕捉 & Thread 自身的捕捉
public static void main(String[] args) {
UncaughtUsage defaultUncaught = new UncaughtUsage("Default Uncaught");
Thread.setDefaultUncaughtExceptionHandler(defaultUncaught);
MyThreadGroup threadGroup = new MyThreadGroup();
Thread t1 = new TestThread(threadGroup, "Thread-1(Use specific uncaught)");
Thread t2 = new TestThread(threadGroup, "Thread-2");
// 多創建一個
UncaughtUsage specificUncaught = new UncaughtUsage("Specific Uncaught");
// 設定給指定 Thread
t1.setUncaughtExceptionHandler(specificUncaught);
t1.start();
t2.start();
}
● Thread 自身的
uncaughtException
函數被呼叫後,不會再呼叫傳遞給 預設、Group 的uncaughtException
更多的 Java 語言相關文章
Java 語言深入
● 在這個系列中,我們全方位地探討了 Java 語言的各個核心主題,旨在幫助你徹底掌握這門強大的編程語言。無論你是想深入理解 Java 的基礎類型與變數作用域,還是探索異常處理與運算子的細節,這些文章都將為您提供寶貴的知識
此外,我們還涵蓋了物件創建、函數式編程、註解應用以及泛型的深入分析,幫助您提升在實際開發中的技能和效率
點擊以下連結,開始你的學習之旅~
● 深入探索 Java 基礎類型、編碼、浮點數、參考類型和變數作用域 | 探討細節
● 深入了解 Java 應用與編譯:從原始檔到命令產出 JavaDoc 文件 | JDK 結構
● 深入理解 Java 運算子與修飾符 | 重要概念、細節 | equals 比較
● 深入探索 Java 物件創建與引用細節:Clone 和finalize 特性,以及強、軟、弱、虛引用
● 認識 Java 函數式編程:從 Lambda 表達式到方法引用 | 3 種方法引用
Java IO 相關文章
● 探索 Java IO 的奧秘,了解檔案操作、流處理、NIO等精彩內容!
深入 Java 多執行緒
● 這一系列文章將帶你深入了解 Java 多執行緒技術的各個方面,從基礎知識到進階應用,涵蓋了多執行緒編程的核心概念與實踐
深入 Java 物件導向
● 探索 Java 物件導向的奧妙,掌握介面、抽象類、繼承等重要概念!幫助你針對物件導向設計有更深入的了解!