owned this note
owned this note
Published
Linked with GitHub
---
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 之前運行 |
> 
:::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());
}
}
```
> 
* **異常結束**
當 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`),但是並不會影響主執行緒的運行
> 
## 任務創建、運行
執行緒它需要去執行一個「任務」,而 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()` 方法後才真正的跟執行緒產生關係**
:::
> 
### Runable 創建任務
* **Runnable 介面** ([Android Runnable API](https://developer.android.com/reference/java/lang/Runnable)):
**Runable 是一種介面(`interface`)**,既然是介面就可以實作(`implememts`)或是創建匿名類,並且在創建出來後,須將其賦予 Thread
> 
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) 類來執行
> 
* **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);
}
}
```
> 
### Thread 運行任務:run、start 差異
* Thread 類中有 `run`、`start` 兩個方法,而這兩個方法看似都可以執行任務,但是它們是有很大的差異的,**`start` 方法是真正讓新執行緒去執行任務**,而 `run` 方法則是讓當前的執行緒去執行任務,兩個方法的概念圖如下
> `run()` 是順序執行(無心執行序運行);`start()` 是同時執行(有執行序運行)
>
> 
* **我們來比較一下兩者個實作差異**:
* 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();
}
}
}
```
> 
:::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` 異常**
> 
:::
## 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);
}
}
```
從結果可見,優先序確實會影響執行序執行
> 
2. **無優先度設置**(把 `setPriority` Mark 起來即可)
> 
### 插入執行緒: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 方法時**,有三個執行序就會互相搶執行順序,這樣的問題會造成無法有效的使用執行序的效率來達成共同任務
> 
:::
1. **在呼叫 `start()` 後立刻呼叫 `join`**:
當前執行的執行序就會掛起(`Blocking`)強制先執行完該加入的執行序的任務,再繼續其他任務
> 以目前來講,就是 MainThread 掛起,先運行 `j1`
>
> 
2. **在呼叫 `start()` 後沒有馬上呼叫 `join`**:
首先,執行緒會跟呼叫 `join()` 之前的執行序互搶 CPU 資源,在呼叫 `join` 之後,正在執行的執行序就會掛起直到目標執行緒執行完任務
> 
### 執行序讓出: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 的線程自己再次搶到**(下圖就是)
> 
:::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);
}
}
}
```
> 
:::
## 守護執行序: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` 才有用,否則會拋出異常
:::
> 
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)結束,背景就會結束
>
> 
## 處理執行序中斷
以往 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();
}
}
```
> 
* **取得中斷訊號,並消除中斷訊號**
* 使用 `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();
}
}
```
> 
* 使用 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();
}
}
```
> 
## 其他
### 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
:::
> 
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.");
}
```
> 
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.");
}
}
```
> 
### 執行序群組: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 中
> 
### 執行序未捕獲的異常
* 從 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();
}
```
> 
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` 函數都會被呼叫**
:::
> 
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`**
:::
> 
## Appendix & FAQ
:::info
:::
###### tags: `Java 基礎進階` `Java 多線程`