Java 執行緒基礎

低吟不作語發表於2021-02-19

本文部分摘自《Java 併發程式設計的藝術》


執行緒簡介

1. 什麼是執行緒?

現代作業系統在執行一個程式時,會為其建立一個程式,一個程式裡可以建立多個執行緒。現代作業系統排程的最小單元是執行緒,也叫輕量級程式。這些執行緒都擁有各自的計數器、堆疊和區域性變數等屬性,並且能訪問共享的記憶體變數。處理器在這些執行緒上高速切換,讓使用者覺得這些執行緒在同時執行

2. 為什麼使用多執行緒?

使用多執行緒的原因主要有以下幾點:

  • 更多的處理器核心

    通過使用多執行緒技術,將計算邏輯分配到多個處理器核心上,可以顯著減少程式的處理時間

  • 更快的響應時間

    有時我們會編寫一些較為複雜的程式碼(主要指業務邏輯),可以使用多執行緒技術,將資料一致性不強的操作派發給其他執行緒處理(也可以使用訊息佇列)。這樣做的好處是響應使用者請求的執行緒能夠儘可能快地處理完成,縮短了響應時間

  • 更好的程式設計模型

    Java 已經為多執行緒程式設計提供了一套良好的程式設計模型,開發人員只需根據問題需要建立合適的模型即可


執行緒優先順序

現代作業系統基本採用時分的形式排程執行的執行緒,作業系統會分出一個個時間片,執行緒分配到若干時間片,當執行緒的時間片用完了發生執行緒排程,並等待下次分配。執行緒分配到的時間片多少也就決定了執行緒使用處理器資源的多少,而執行緒優先順序就是決定執行緒需要多或少分配一些處理器資源的執行緒屬性

在 Java 執行緒中,通過一個整型成員變數 priority 來控制優先順序,優先順序的範圍從 1 ~ 10,線上程構建時可以通過 setPriority(int) 方法來修改優先順序,預設優先順序是 5,優先順序高的執行緒分配時間片的數量要多於優先順序低的執行緒。不過,在不同的 JVM 以及作業系統上,執行緒規劃會存在差異,有些作業系統甚至會忽略執行緒優先順序的設定

public class Priority {

    private static volatile boolean notStart = true;
    private static volatile boolean notEnd = true;

    public static void main(String[] args) throws Exception {
        List<Job> jobs = new ArrayList<Job>();
        for (int i = 0; i < 10; i++) {
            int priority = i < 5 ? Thread.MIN_PRIORITY : Thread.MAX_PRIORITY;
            Job job = new Job(priority);
            jobs.add(job);
            Thread thread = new Thread(job, "Thread:" + i);
            thread.setPriority(priority);
            thread.start();
        }
        notStart = false;
        TimeUnit.SECONDS.sleep(10);
        notEnd = false;
        for (Job job : jobs) {
            System.out.println("Job Priority : " + job.priority + ", Count : " + job.jobCount);
        }
    }

    static class Job implements Runnable {
        private int priority;
        private long jobCount;

        public Job(int priority) {
            this.priority = priority;
        }

        @Override
        public void run() {
            while (notStart) {
                Thread.yield();
            }
            while (notEnd) {
                Thread.yield();
                jobCount++;
            }
        }
    }
}

執行該示例,在筆者機器上對應的輸出如下

筆者使用的環境為:Win10 + JDK11,從輸出可以看到執行緒優先順序起作用了


執行緒的狀態

Java 執行緒在執行的生命週期中可能處於下表所示的六種不同的狀態,在給定的一個時刻,執行緒只能處於其中的一個狀態

狀態名稱 說明
NEW 初始狀態,執行緒被構建,但還沒呼叫 start() 方法
RUNNABLE 執行狀態,Java 執行緒將作業系統中的就緒和執行兩種狀態籠統地稱作“執行中”
BLOCKED 阻塞狀態,表示執行緒阻塞於鎖
WAITING 等待狀態,表示執行緒進入等待狀態,進入該狀態表示當前執行緒需要等待其他執行緒做出一些特定動作(通知或中斷)
TIME_WAITING 超時等待狀態,該狀態不同於 WAITING,它是可以在指定的時間自行返回的
TERMINATED 終止狀態,表示當前執行緒已經執行完畢

執行緒在自身的生命週期中,並不是固定地處於某一狀態,而是隨著程式碼的執行在不同的狀態之間進行切換


Daemon 執行緒

Daemon 執行緒是一種支援型執行緒,主要被用作程式中後臺排程以及支援性工作。這意味著,當一個 Java 虛擬機器中不存在 Daemon 執行緒的時候,Java 虛擬機器將退出。可以呼叫 Thread.setDaemon(true) 將執行緒設定為 Daemon 執行緒

使用 Daemon 執行緒需要注意兩點:

  • Daemon 屬性需要在啟動執行緒之前設定,不能在啟動執行緒之後設定
  • 在構建 Daemon 執行緒時,不能依靠 finally 塊中的內容來確保執行或關閉清理資源的邏輯。因為在 Java 虛擬機器退出時 Daemon 執行緒中的 finally 塊並不一定會執行

啟動和終止執行緒

1. 構造執行緒

在執行執行緒之前首先要構造一個執行緒物件,執行緒物件在構造的時候需提供執行緒需的屬性,如執行緒所屬的執行緒組、是否是 Daemon 執行緒等資訊

2. 啟動執行緒

執行緒物件在初始化完成之後,呼叫 start() 方法即可啟動執行緒

3. 理解中斷

中斷可以理解為執行緒的一個標識位屬性,標識一個執行中的執行緒是否被其他執行緒進行了中斷操作。中斷好比其他執行緒對該執行緒打了個招呼,其他執行緒可以通過呼叫該執行緒的 interrupt() 方法對其進行中斷操作

執行緒通過檢查自身是否被中斷進行響應,執行緒通過 isInterrupted() 來進行判斷是否被中斷,也可以呼叫靜態方法 Tread.interrupted() 對當前執行緒的中斷標識位進行復位。如果執行緒已經處於終結狀態,即時執行緒被中斷過,在呼叫該物件的 isInterrupted() 時依舊會返回 false

許多宣告丟擲 InterruptedException 的方法在丟擲異常之前,Java 虛擬機器會先將該執行緒的中斷標識位清除,然後丟擲 InterruptedException,此時呼叫 isInterrupted() 方法將會返回 false

在下面的例子中,首先建立兩個執行緒 SleepThread 和 BusyThread,前者不停地睡眠,後者一直執行,分別對兩個執行緒分別進行中斷操作,觀察中斷標識位

public class Interrupted {

    public static void main(String[] args) throws InterruptedException {
        // sleepThread 不停的嘗試睡眠
        Thread sleepThread = new Thread(new SleepRunner(), "SleepThread");
        sleepThread.setDaemon(true);
        // busyThread 不停的執行
        Thread busyThread = new Thread(new BusyRunner(), "BusyThread");
        busyThread.setDaemon(true);
        sleepThread.start();
        busyThread.start();
        // 休眠 5 秒,讓 sleepThread 和 busyThread 充分執行
        TimeUnit.SECONDS.sleep(5);
        sleepThread.interrupt();
        busyThread.interrupt();
        System.out.println("SleepThread interrupted is " + sleepThread.isInterrupted());
        System.out.println("BusyThread interrupted is " + busyThread.isInterrupted());
        // 防止 sleepThread 和 busyThreaad 立刻退出
        SleepUtils.second(2);
    }

    static class SleepRunner implements Runnable {

        @Override
        public void run() {
            while (true) {
                SleepUtils.second(10);
            }
        }
    }

    static class BusyRunner implements Runnable {

        @Override
        public void run() {
            while (true) {

            }
        }
    }
}

輸出如下

從結果可以看出,丟擲 InterruptedException 的執行緒 SleepThread,其中斷標識位被清除了,而一直忙碌執行的執行緒 BusyThread 的中斷標識位沒有被清除

4. 安全地終止執行緒

前面提到的中斷操作是一種簡便的執行緒間互動方式,適合用來取消或停止任務。除了中斷以外,還可以利用一個 boolean 變數來控制是否需要停止任務並終止執行緒

下面的示例中,建立了一個執行緒 CountThread,它不斷地進行變數累加,而主執行緒嘗試對其進行中斷操作和停止操作

public class Shutdown {

    public static void main(String[] args) throws InterruptedException {
        Runner one = new Runner();
        Thread countThread = new Thread(one, "CountThread");
        countThread.start();
        // 睡眠一秒,main 執行緒對 CountThread 進行中斷,使 CountThread 能夠感知中斷而結束
        TimeUnit.SECONDS.sleep(1);
        countThread.interrupt();
        Runner two = new Runner();
        countThread = new Thread(two, "CountThread");
        countThread.start();
        // 睡眠一秒,main 執行緒對 Runner two 進行中斷,使 CountThread 能夠感知 on 為 false 而結束
        TimeUnit.SECONDS.sleep(1);
        two.cancel();
    }

    private static class Runner implements Runnable {

        private long i;
        private volatile boolean on = true;

        @Override
        public void run() {
            while (on && !Thread.currentThread().isInterrupted()) {
                i++;
            }
            System.out.println("Count i = " + i);
        }

        public void cancel() {
            on = false;
        }
    }
}

main 執行緒通過中斷操作和 cancel() 方法均可使 CountThread 得以終止。這種通過標識位或者中斷操作的方式能夠使執行緒在終止時有機會去清理資源,而不是武斷地將執行緒停止,更加安全和優雅


相關文章