初探 Thread 與 Process:掌握 Java 多執行緒技術的常規基礎應用 | 並行、併發

初探 Thread 與 Process:掌握 Java 多執行緒技術的常規基礎應用 | 並行、併發

Overview of Content

在現今高效能運算的時代,了解 Thread 和 Process 的運作機制成為每個軟體開發者的必備技能。本篇文章將帶你全面掌握 Java 多執行緒技術,深入解析從基礎概念到高級應用的每個重要環節

首先,我們將探討 Thread 與 Process 的基礎概念,了解軟、硬體 Process 和多執行緒技術的基本原理。接著,我們會比較併發與並行的差異,分析多執行緒的優缺點,幫助你更好地選擇適合的開發模式

隨後,我們會深入 Java Thread 的觀念,包括 Thread 的狀態圖和生命週期的結束過程,讓你清楚了解每個 Thread 的生命歷程。接著,我們將討論各種創建和運行任務的方法,從 ThreadRunableCallable,並比較說明 run 和 start 的差異。

在排程模型部分,你將學會如何調整 Thread 的優先序、插入執行緒及執行序讓出的操作,確保任務的高效調度。對於守護執行序(Daemon)和執行序中斷的處理,我們也將提供詳細的實踐指導,確保你能應對各種複雜的執行緒操作。

最後,我們將介紹一些其他重要的執行緒相關知識,如 Timer 計時器、執行序群組(ThreadGroup)以及未捕獲的異常處理,讓你全面提升多執行緒編程能力。

以下可能會混用 “線程”、“執行序”,兩者是相同意思

寫文章分享不易,如有引用參考請詳註出處,如有指導、意見歡迎留言(如果覺得寫得好也請給我一些支持),感謝 😀

個人程式分享時比較注重「縮排」,所以可能不適合手機的排版閱讀,建議切換至「電腦版」、「平板版」視窗看


Thread & Process 基礎概念

認識軟、硬體 Process

● Process 這個詞可以使用在「硬體」、「軟體」兩個領域內,而兩種是不同的概念

「硬體」的 Process 概念

單芯片多處理器 Chip MultiProcess (CMP),其思想是將 並行處理器 中的 對稱多處理器 (SMP)集合到一個 IC 中,也就是兩個或更多獨立處理器封裝在一個單一積體電路(IC)中

使用「多核心」指在同一積體電路中整合多個獨立處理器的 CPU (即「多核心處理器」)

graph LR subgraph IC 晶片 處理器_CPU_A 處理器_CPU_B 處理器_CPU_C end

「軟體」的 Process 概念

軟體的 Process 也就是行程(或稱之為進程),這是系統規劃「每個應用的最小單位」,並且每個 Process 都會有一個完整個虛擬記憶體空間

而這個 Process 的資料(記憶體、暫存器等等)則是由核心空間做管理… 概念圖如下

graph LR 應用A 應用B 應用C subgraph 核心空間 ProcessA ProcessB ProcessC end CPU -.-> ProcessA --> 應用A CPU -.-> ProcessB --> 應用B CPU -.-> ProcessC --> 應用C

認識 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):它能真正意義上的做到同時執行,可並排處理不同事務,概念圖如下

graph LR subgraph 同一個時間點 應用_A 應用_B 應用_C 應用_D end CPU_A --> 應用_A CPU_B --> 應用_B CPU_C --> 應用_C CPU_D --> 應用_D

● 併發(Thread): 談論併發時一定要 加上時間的限制 (單位時間的併發量);這是內核機制所做出的功能,它可以增加 CPU 的吞吐量(如果運用得當的話)

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

● 離開了時間單位的話談論併發是沒有意義的

實現併發技術相當複雜,最容易理解的就是時間輪轉機制,以快速切換來達成同時處理的假象

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)runstart這兩個函數的差異,後續會再提及;主要到該階段,Thread 就可以調用處理任務
休眠(Blocking)sleepyieldCPU 休眠(不耗費 CPU 時間)
等待(Blocking)Object#waitCPU 休眠(不耗費 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 處理,網路請求... 等等

graph LR 執行緒 -.-> |運行任務| Thread 執行緒 -.-> |運行任務| Runnable 執行緒 -.-> |運行任務| Callable

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 如下所示

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 : 聚合

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 類中有 runstart 兩個方法,而這兩個方法看似都可以執行任務,但是它們是有很大的差異的,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_PRIORITYMIN_PRIORITYNORM_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 有一些方法可以處理執行序中斷,像是 suspendresumestop …等等,但這些方法都被 Deprecated

● 這些 API 被棄用的原因

如果 suspend 一個持有「鎖」的執行序,會導致它在 resume 之前都無法釋放鎖,可能導致死鎖

suspend 在暫停執行序(Thread)時不會釋放鎖

「鎖」請參考另一篇文章 Java 多執行序 - 同步、鎖

正確處理 interrupt 中斷

● 現在建議的方法是 使用 interrupt(),拋出個訊號,讓使用者自行斷定是否該停止

中斷信號!

interrupt() 訊號如果 InterruptedExceptionThreadinterrupted 抓住後就會清除中斷訊號

● 如果使用 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 物件導向的奧妙,掌握介面、抽象類、繼承等重要概念!幫助你針對物件導向設計有更深入的了解!

Leave a Comment

Comments

No comments yet. Why don’t you start the discussion?

發表迴響