--- title: 'Java 多執行序' disqus: kyleAlien --- Java 多執行序 === ## Overview of Content 如有引用參考請詳註出處,感謝 :smile: > 以下可能會混用 “線程”、“執行序”,兩者是相同意思 :::success * 如果喜歡讀更好看一點的網頁版本,可以到我新做的網站 [**DevTech Ascendancy Hub**](https://devtechascendancy.com/) 本篇文章對應的是 [**初探 Thread 與 Process:掌握 Java 多執行緒技術的常規基礎應用 | 並行、併發**](https://devtechascendancy.com/multithread-sync-cas-thread-local-guide/) ::: [TOC] ## Thread & Process 基礎概念 ### 認識軟、硬體 Process * Process 這個詞可以使用在「硬體」、「軟體」兩個領域內,而兩種是不同的概念 * **「硬體」的 Process 概念**: 單芯片多處理器 `Chip MultiProcess` [(CMP)](https://zh.wikipedia.org/wiki/多核心處理器),其思想是將 **並行處理器** 中的 **對稱多處理器 [(SMP)](https://zh.wikipedia.org/wiki/对称多处理)集合到一個 IC 中**,也就是兩個或更多獨立處理器封裝在一個單一積體電路(IC)中 使用「多核心」指在**同一積體電路中整合多個獨立處理器的 CPU** (即「多核心處理器」) ```mermaid graph LR subgraph IC 晶片 處理器_CPU_A 處理器_CPU_B 處理器_CPU_C end ``` * **「軟體」的 Process 概念**: 軟體的 Process 也就是行程(或稱之為進程),這是系統規劃「**每個應用的最小單位**」,並且每個 Process 都會有一個完整個虛擬記憶體空間 而這個 Process 的資料(記憶體、暫存器等等)則是由核心空間做管理… 概念圖如下 > Process 算是系統對於 CPU 特性應用的規劃,將每個應用抽象為「Process」來管理 ```mermaid graph LR 應用A 應用B 應用C subgraph 核心空間 ProcessA ProcessB ProcessC end CPU -.-> ProcessA --> 應用A CPU -.-> ProcessB --> 應用B CPU -.-> ProcessC --> 應用C ``` ### 認識 Thread 多執行序 * **多執行序**(也稱為多線程): `Simultaneous Multithreading` [(SMT)](https://zh.wikipedia.org/wiki/多线程) 可複製處理器上的結構狀態,「**共享處理器的資源**」,執行緒之間進行切換 **由於時間間隔很小,來給用戶造成一種多個執行緒同時執行的 ++假象++** > 也就是其實單核心執行緒也可以達到 Multithreading 的使用,它就是透過快速切換來達成同時執行的假象 :::info * 快速切換的手法 **[時間片輪轉機制](http://www.baike.com/wiki/时间片轮转调度算法)**(**RR 調度**) 時間片輪轉機制就是設定一個固定的 CPU 時間,當時間一到就進行中斷處理,切換 CPU 資源給另一個執行緒 * 如果在時間片結束時執行序還在運行, 則會暫停該執行緒,並將 CPU 分配給另一個執行序 * 如果執行序在時間片結束前阻塞,則 CPU當即進行切換 > 這種時間片輪詢機制是由內核(軟體)安排的機制,並非 CPU 自身機制(也就說是系統在利用 CPU 資源) ::: :::success * **執行緒上下文切換 `Context switch`** 由於 CPU 並不會紀錄 Thread 的相關資訊(依照 CPU 的特性,它也不應該拿來紀錄 Thread 相關資訊),這些資訊應該由內核紀錄 而切換 Thread 時就必須紀錄、載入資訊,這些資訊稱為 **執行緒上下文** > 切換要做的事情 : 保存和裝入暫存器、內存映像,更新各種表格、隊列 ::: ### Thread & Process 的差異:併發 & 並行概念 * **Process 進程**:(這裡談論的是軟體 Process) **應用資源的最小單位**,每個 Process 都擁有獨立的虛擬記憶體,這些虛擬記憶體之間無法相互訪問、交換資源(CPU、暫存器空間、磁盤... 等等) > 它是一個獨立單位,並不會被其他進程影響,並且資源不共享(如果需要共享則需要利用 Socket、內存共享、文件、Binder… 等等) * **Thread 執行序** : **處理器(`CPU`) 調度的最小單位**;它的特性是可以共享內存地址,這也就意味著資源共享;擁有一些私有資源(暫存器(局部變量)、堆棧) :::warning 它的好處是足夠快速,操作相對於進程通訊來說也較為方便簡單;但是資源的爭奪不同步又是另一個大問題 ::: * 另外,我們在軟體開發中會以「**並行**」、「**併**」兩個詞彙來描述 Process、Thread 的兩種特性;簡單的理解就是,**並行資源不共享(`Process`),併資源共享(`Thread`)** * 並行(Process):**它能真正意義上的做到同時執行**,可並排處理不同事務,概念圖如下 ```mermaid graph LR subgraph 同一個時間點 應用_A 應用_B 應用_C 應用_D end CPU_A --> 應用_A CPU_B --> 應用_B CPU_C --> 應用_C CPU_D --> 應用_D ``` * 併發(Thread): 談論併發時一定要 **加上時間的限制** (單位時間的併發量);這是內核機制所做出的功能,它可以增加 CPU 的吞吐量(如果運用得當的話) ```mermaid graph LR CPU_A subgraph 時間點A 應用_A end subgraph 時間點B 應用_B end subgraph 時間點C 應用_C end CPU_A -.-> 應用_A CPU_A -.-> 應用_B CPU_A -.-> 應用_C ``` :::info * 離開了時間單位的話談論併發是沒有意義的 實現併發技術相當複雜,**最容易理解的就是時間輪轉機制**,以快速切換來達成同時處理的**假象** ::: ### Thread 的優缺點 * 使用 Thread 的優點: 1. 充分利用 CPU 資源(當然,必須要有適當的規劃,否則任意使用 Thread 也會導致效能不佳) 2. 加快了用戶的響應時間,在用戶使用當前資源時,在後端同時間加載其它資源 3. 讓代碼 模塊、異步、簡單化 : 可獨立化一個代碼區塊,方便日後維護 * 雖然 Thread 很方便,但它仍有需要注意的使用點,而使用 Thread 的注意事項如下: 1. **線程不安全**: 由於共享資源,在寫入操作時會有同步問題(讀取不會有問題),處理不好也會影響效能 2. **線程死鎖**: 如果有兩把鎖,要共同取得才能操作的話,不同線程持有不同鎖,而且都不釋放 3. **線程過多**: 線程切換需要時間,如果過多線程會造成**過度切換**,造成死機 (可用線程池解決) ## Java Thread 觀念 **Thread 類是 Java 對執行序概念的抽象** ### Thread 狀態圖 * 了解 Thread 的狀態相當重要(可以之後再來反覆查看),Java 的每個方法操作都會觸發 Thread 處於不同狀態,不同狀態下的 Thread 又會有不同特性 | 狀態 | 觸發該狀態的函數 | 補充 | | - | - | - | | 新建(`New`) | 創建 Thread 物件 | 目前仍運行在 | | 就緒(`Runnable`) | `run`、`start` | 這兩個函數的差異,後續會再提及;主要到該階段,Thread 就可以調用處理任務 | | 休眠(`Blocking`) | `sleep`、`yield` | CPU 休眠(不耗費 CPU 時間) | | 等待(`Blocking`) | `Object#wait` | CPU 休眠(不耗費 CPU 時間),通常用於 Thread 通訊作用 | | 執行(`Running`) | `join` | 將當前任務插入到指定 Thread 之前運行 | > ![](https://i.imgur.com/JhJNm0k.png) :::warning * **Thread#`sleep` & Object#`wait` 並不耗費 CPU 時間** ::: ### Thread 生命週期結束 * Thread 生命週期結束就是 Thread 結束生命週期,而生命週期結束一般來說有兩種方式 ^1.^ 正常結束、^2.^ 異常結束 * **正常結束** 可以使用 Thread#`isAlive` 方法判斷 Thread 是否以經結束生命週期 ```java= 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()); } } ``` > ![](https://hackmd.io/_uploads/B1nQD6r0n.png) * **異常結束** 當 Thread 運行時(我們可稱之為 WorkThread)發生異常並不會影響主執行緒,主執行緒仍可正常執行 ```java= 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`),但是並不會影響主執行緒的運行 > ![](https://hackmd.io/_uploads/BJu-dpBRn.png) ## 任務創建、運行 執行緒它需要去執行一個「任務」,而 Java 對於執行緒的任務創建有三種基礎的方式,相關類有 ^1.^ `Thread`(class)、^2.^ `Runnable`(interface)、^3.^ `Callable`(generic interface) > Thread 運行的任務大多都是耗時任務,像是 IO 處理,網路請求... 等等 ```mermaid graph LR 執行緒 -.-> |運行任務| Thread 執行緒 -.-> |運行任務| Runnable 執行緒 -.-> |運行任務| Callable ``` ### Thread 創建任務 * **Thread 類**([**Android Thread API**](https://developer.android.com/reference/java/lang/Thread)): Thread 本身就是 Java 對執行緒的抽象,而它本身內部就會帶有一個任務函數 `run()`… 我們可以透過 **繼承 Thread 並複寫 `run` 方法** 並在內部撰寫一些耗時任務 ```java= 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(); } } ``` :::warning * 建立完 Thread 物件後,就開始執行了嗎? `new Thread()` 可以建立一個 Thread 實例,但並未真正的跟執行序產生關係,**在執行 `start()` 方法後才真正的跟執行緒產生關係** ::: > ![image](https://hackmd.io/_uploads/rkq0kEEm0.png) ### Runable 創建任務 * **Runnable 介面** ([Android Runnable API](https://developer.android.com/reference/java/lang/Runnable)): **Runable 是一種介面(`interface`)**,既然是介面就可以實作(`implememts`)或是創建匿名類,並且在創建出來後,須將其賦予 Thread > ![](https://hackmd.io/_uploads/ByF66USAn.png) Thread 相較於 Runnable 來說,更加的「輕量級」,這個原因是因為 Java 的單繼承特性導致;由於單繼承會影響到類的繼承只能選擇一個,而介面(`interfcae`)不同,一個類可以實作多個介面,這才導致我們覺得 Runnable 更加簡便 1. 類實作 Runnable 介面,創建 Thread 的任務 ```java= class RunUsage implements Runnable { @Override public void run() { // Do something } public static void main(String[] args) { Thread t = new Thread(new RunUsage()); t.start(); } } ``` 2. 使用匿名類實作 Runnable 介面,同樣可以創建 Thread 任務 ```java= 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](https://developer.android.com/reference/java/util/concurrent/Callable)): * **Callable 是一個泛型介面**(`generic interface`)我們無法直接使用 Thread 來運行 Callable 創建的任務,它必須透過 [**FutureTask**](https://developer.android.com/reference/java/util/concurrent/FutureTask.html) 類來執行 > ![image](https://hackmd.io/_uploads/rkywmN4QC.png) * **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](https://developer.android.com/reference/java/util/concurrent/Future.html) 介面來達到異步任務的空士(可取得結果 or 取消);FutureTask 與 Future、Callback、Runnable 的 UML 如下所示 > ```mermaid classDiagram class Future { <<interface>> bool cancel(mayInterruptIfRunning) boolean isCancelled() boolean isDone() V get() V get(long timeout, TimeUnit unit) } class Runnable { <<interface>> void run() } Future <|-- RunnableFuture Runnable <|-- RunnableFuture RunnableFuture <|.. FutureTask : 實作 Callable o.. FutureTask : 聚合 ``` ```java= 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); } } ``` > ![image](https://hackmd.io/_uploads/B1DcF4EXR.png) ### Thread 運行任務:run、start 差異 * Thread 類中有 `run`、`start` 兩個方法,而這兩個方法看似都可以執行任務,但是它們是有很大的差異的,**`start` 方法是真正讓新執行緒去執行任務**,而 `run` 方法則是讓當前的執行緒去執行任務,兩個方法的概念圖如下 > `run()` 是順序執行(無心執行序運行);`start()` 是同時執行(有執行序運行) > > ![](https://i.imgur.com/OdRNh3d.png) * **我們來比較一下兩者個實作差異**: * Thread# **`run()` 方法**: 業務邏輯實現的地方,也就是執行緒要做的事情,通常是一些耗時算法,也就是我們上面小節說的「任務」) ,從以下源碼中我們可以看到 run 就如同一般的方法,沒有切換執行緒的動作 > 並且 run 方法可以重複執行 ```java= // 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(); } } } ``` > ![](https://hackmd.io/_uploads/SyGi3wHCh.png) :::info * 哪個 Thread 呼叫 `run()` 方法,該方法就在哪個 Thread 運行 ::: * Thread# **`start()` 方法**:**真正讓新建的執行序運行任務** `start()` 方法會讓一個 Thread 的 **狀態轉為就緒** 接著等待 CPU 分配(真正被調用的時機不確定,這由系統決定如何分配),**分配到後才會由新的執行序調用 Thread 的 `run()` 方法**,也就是這時運行 `run()` 方法的執行序不在是之前的執行序,而是新的執行序 在 `start()` 前一直都是使用過往的執行緒 (呼叫的舊執行緒),真正意義創造新執行緒是在 **`nativeCreate` 本地方法中** > 每個版本的 JDK 對於 Thread#start 方法的實現都有些微不同 ```java= // 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 */ } } } } ``` :::warning * 在使用 `Thread` 類時要注意幾件事: 1. 不要隨意 Override `start` 方法! 2. **`start` 方法不可重複呼叫**,否則會 **拋出 `IllegalThreadStateException` 異常** > ![](https://hackmd.io/_uploads/BydZevSC3.png) ::: ## Thread 排程 - 排程模型 JVM 的其中一個任務就是負責執行序(`Thread`)的排程;而我們常見的排程模型有兩種 1. **分時式排程模型** 每個執行緒擁有相同(公平)的 CPU 時間片,用來平均佔用 CPU 時間片 2. **搶佔式排程模型** **Java 採用的排程模式**;該模式會依照 ^1.^ **執行緒的優先順序進行排程**,如果相同則隨機、^2.^ **執行序會一直執行直到**,無法執行! :::info * **執行序無法繼續執行的原因** * JVM 控制暫時放棄(包含主動、被動),那執行序會轉為 **就緒狀態** * 執行序進入 Blocking 狀態(可能在等待協作) * 執行序自然結束 :::danger * 這也與平台有相關 由於執行緒不是跨平台的,所以 **Thread 的順序並非只取決於 JVM,同時也會依賴作業系統** ::: ::: ### 調整 Thread 優先序:priority * 當多個線程處理就緒(`Runnable`)狀態,那 JVM 會先依照 Thread 的優先序進行線程的排序! | Thread 優先序選擇 | 概述 | | - | - | | `MAX_PRIORITY` | 最高優先 | | `MIN_PRIORITY` | 最低優先 | | `NORM_PRIORITY` | 普通優先| :::warning * 由於個 **作業系統平台對於 Thread 的優先序不同**,所以 JVM 有時候不能很好的映射,所以建議都使用 JVM 提供的 `MAX_PRIORITY`、`MIN_PRIORITY`、`NORM_PRIORITY` 設置 > 像是 Window 只有 `7` 個順序,而 Sun 公司的作業系統 Solaris 則有 `231` 個優先序可以控制 ::: 1. **優先度設置** ```java= 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); } } ``` 從結果可見,優先序確實會影響執行序執行 > ![](https://hackmd.io/_uploads/ryLYWCBCn.png) 2. **無優先度設置**(把 `setPriority` Mark 起來即可) > ![](https://hackmd.io/_uploads/BJ76ZRBAh.png) ### 插入執行緒:join * Thread 的 join 方法,**可以將目前執行中的執行序轉到掛起狀態(`Blocking`),直到另一個執行序結束,它才會恢復(`Running`)** ```java= 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); } } } ``` :::info * **當未使用 Join 方法時**,有三個執行序就會互相搶執行順序,這樣的問題會造成無法有效的使用執行序的效率來達成共同任務 > ![](https://hackmd.io/_uploads/SyEcxQvAh.png) ::: 1. **在呼叫 `start()` 後立刻呼叫 `join`**: 當前執行的執行序就會掛起(`Blocking`)強制先執行完該加入的執行序的任務,再繼續其他任務 > 以目前來講,就是 MainThread 掛起,先運行 `j1` > > ![](https://i.imgur.com/Z5WQOMi.png) 2. **在呼叫 `start()` 後沒有馬上呼叫 `join`**: 首先,執行緒會跟呼叫 `join()` 之前的執行序互搶 CPU 資源,在呼叫 `join` 之後,正在執行的執行序就會掛起直到目標執行緒執行完任務 > ![](https://i.imgur.com/Q7hui5i.png) ### 執行序讓出:yield * **使用 `Thread`#`yield` 函數可以讓當前執行序主動讓出 CPU 執行時間**(讓出 CPU 使用的時間),JVM 就會讓所有未掛起的執行序來搶奪 CPU 資源 :::info * 讓出後,哪個執行序會搶到 CPU 資源? 這不一定!可能由讓出的執行序重新獲得,或是由其他的執行序獲得 ::: **--實做--** ```java= 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 的線程自己再次搶到**(下圖就是) > ![](https://i.imgur.com/kprWip5.png) :::success * **執行序的 `Yield`、優先度**: `Yield` 方法只會將 CPU 執行權力讓給 **同優先級別、更高優先級別的執行序**(不會讓給低優先度的執行序) > 相對來說 `sleep` 方法就公平分配,低優先度仍可搶奪 CPU 資源 ```java= 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); } } } ``` > ![](https://hackmd.io/_uploads/SkUMU7P0h.png) ::: ## 守護執行序:Daemon * **守護執行序**:(也稱之為背景執行序) 背景執行序的特點在於,**背景執行緒會與前景執行序生命週期相伴!** **只有所有前景執行序都結束後背景執行序才會結束** :::info **JVM 的 GC 就是典型的背景執行序** ::: 1. **Thread#`setDaemon` 設定為 `false`** (預設值):也就是非守護執行序,只是一般的執行序 ```java= 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`)生命週期相伴,前景執行序會先結束,接著背景執行序繼續執行,**直到背景執行序也執行結束才會結束整個應用的生命週期** :::warning 必須在執行緒啟動前(`start` 之前)設置 `setDaemon` 才有用,否則會拋出異常 ::: > ![](https://hackmd.io/_uploads/S1hpDmw02.png) 2. **Thread#`setDaemon` 設定為 `true`**(設定為守護執行序):以下讓主執行序創建另一個執行序(`WorkThread`),並將 WorkThread 設定為守護執行序 > 可以想像為,讓 WorkThread 守護主執行序,主執行序結束 WorkThread 就一起結束(以主執行序生命週期為主) ```java= 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)結束,背景就會結束 > > ![](https://hackmd.io/_uploads/By9X_QPA3.png) ## 處理執行序中斷 以往 Thread 有一些方法可以處理執行序中斷,像是 `suspend`、`resume`、`stop` …等等,但這些方法都被 `Deprecated` 了 :::warning * 這些 API 被棄用的原因 如果 `suspend` 一個持有「鎖」的執行序,會導致它在 `resume` 之前都無法釋放鎖,可能導致死鎖 > `suspend` 在暫停執行序(Thread)時不會釋放鎖 「鎖」請參考另一篇文章 [**Java 多執行序 - 同步、鎖**](https://hackmd.io/7Ru0TE45Tnm1LEUqx4qe-A?view#Java-%E5%A4%9A%E5%9F%B7%E8%A1%8C%E5%BA%8F---%E5%90%8C%E6%AD%A5%E3%80%81%E9%8E%96) ::: ### 正確處理 interrupt 中斷 * 現在建議的方法是 **使用 `interrupt()`**,拋出個訊號,讓使用者自行斷定是否該停止 :::success * **中斷信號!** * `interrupt()` 訊號如果 **被 `InterruptedExceptionThread`、`interrupted` 抓住後就會清除中斷訊號** * 如果使用 `isInterrupted()` 中斷訊號則不會被清除 ::: * **取得中斷訊號,但不清除** ```java= 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(); } } ``` > ![](https://hackmd.io/_uploads/rJlvD_UdA3.png) * **取得中斷訊號,並消除中斷訊號** * 使用 `Thread#interrupted` 這個靜態方法消除中斷訊號 ```java= 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(); } } ``` > ![](https://hackmd.io/_uploads/BkifKLOAh.png) * 使用 try/catch 捕捉 `InterruptedException` 異常!(這種捕捉動作同時會消除中斷訊號~) ```java= 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(); } } ``` > ![](https://hackmd.io/_uploads/B13PF8dRn.png) ## 其他 ### Timer 計時器 * 首先 **Timer 這個類本身 ++並非執行序(Thread)的衍生類++**!它是 Runnable 的實作者 ```java= public abstract class TimerTask implements Runnable { ... } ``` * Timer 的使用特色 1. **它會一直運作**:直到呼叫 `cancel` 手動將其關閉,或是發生異常 2. 可以設定是否是守護(背景)執行序 ```java= 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."); } } ``` :::info * 如果沒有 Cancel 則會一直阻塞住 Blocking ::: > ![](https://hackmd.io/_uploads/BkxGHS_A3.png) 3. **Timer 在啟動後會自己創建一個執行序**,不用你手動創建 ```java= 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."); } ``` > ![](https://hackmd.io/_uploads/rJWNPBuAh.png) 4. 可以循環(週期)運作 ```java= 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."); } } ``` > ![](https://hackmd.io/_uploads/ByhGUruR3.png) ### 執行序群組:ThreadGroup * **ThreadGroup 可以用來「管理」一系列「存活」的執行序**,如果該執行序已經執行完(生命週期結束),那就會被從群組中移除 :::info * JVM 執行應用時會建立一個名為「`main`」的執行群組!**在預設行況下,所有執行序都屬於「main」群組** ::: ```java= 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 中 > ![](https://hackmd.io/_uploads/HkNWyv_0n.png) ### 執行序未捕獲的異常 * 從 JDK 1.5 版本開始,有加強對執行序的例外處理;如果執行序沒有捕獲例外,JVM 會尋找應用中的 `UncaughtExceptionHandler` 實體並對其他發送異常 * 而 `UncaughtExceptionHandler` 的使用是需要註冊的,並有處理順序,未捕獲異常的順序如下 1. Thread 自身的 `UncaughtExceptionHandler`(**優先度最高,處理過後不會再傳入 `ThreadGroup` & `Default`**) ```java= Thread t1 = new Thread(); UncaughtUsage uncaughtUsage = new Thread.UncaughtExceptionHandler() { @Override public void uncaughtException(Thread t, Throwable e) { ... } } t1.setUncaughtExceptionHandler(uncaughtUsage); ``` 2. 如果該執行序有群組,則執行群組的 `UncaughtExceptionHandler` ```java= new ThreadGroup { @Override public void uncaughtException(Thread t, Throwable e) { super.uncaughtException(t, e); } } ``` 3. 預設的 `UncaughtExceptionHandler`(也就是靜態 Thread#setDefaultUncaughtExceptionHandler 函數設定) ```java= UncaughtUsage uncaughtUsage = new Thread.UncaughtExceptionHandler() { @Override public void uncaughtException(Thread t, Throwable e) { ... } } Thread.setDefaultUncaughtExceptionHandler(uncaughtUsage); ``` 4. 都沒有捕捉,則往 `System.err` 的標準輸出(優先度最低) * **使用範例**: ```java= 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."); } } ``` 1. **使用預設捕捉** ```java= 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(); } ``` > ![](https://hackmd.io/_uploads/SykEZduR3.png) 2. **使用設定的預設捕捉 & ThreadGroup 的捕捉** ```java= 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(); } ``` :::info * **兩個 `uncaughtException` 函數都會被呼叫** ::: > ![](https://hackmd.io/_uploads/HJROZOd02.png) 3. **使用設定的預設捕捉 & ThreadGroup 的捕捉 & Thread 自身的捕捉** ```java= 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(); } ``` :::warning * **Thread 自身的 `uncaughtException` 函數被呼叫後,不會再呼叫傳遞給 預設、Group 的 `uncaughtException`** ::: > ![](https://hackmd.io/_uploads/HyY0kO_0h.png) ## Appendix & FAQ :::info ::: ###### tags: `Java 基礎進階` `Java 多線程`