1. 程式人生 > 其它 >Java 併發程式設計二 (Thread)

Java 併發程式設計二 (Thread)

執行緒狀態

執行緒一般的狀態轉換圖如下:

線上程生命週期中存在的狀態解釋如下:

  • New(初始化)狀態

    此時執行緒剛剛被例項化,可以通過呼叫 start() 方法來啟動這個例項化的的執行緒,使其狀態轉變成為 Ready 狀態

  • Runnable 狀態

    Ready 狀態和 Running 狀態統稱為 Runnable 狀態

    • Ready (就緒)狀態

      此時執行緒已經可以被作業系統排程了,但是此時還沒有執行,當被作業系統排程,獲得 CPU 的執行許可權後,此時執行緒的狀態就轉變成為 Ready 狀態。

    • Running 狀態

      此時執行緒獲得了 CPU 的時間片,正在執行中

  • Blocked

    (阻塞)狀態

    此時執行緒由於某種原因(獲取鎖、IO 等)無法利用 CPU

  • Wating(等待)狀態

    此時執行緒由於某些原因,需要等待其它的執行緒執行完成之後才能繼續執行,與阻塞狀態不同的地方在與,等待狀態是自己主動地等待某個操作完成,而阻塞則是由於一些不可控的外在因素被動地等待

  • TIMED_WATING(超時等待)狀態

    和等待狀態類似,但是與之不同的地方在於超時等待狀態會在指定的時間之後自行結束等待

  • Terminated(終止)狀態

    該執行緒的任務已經完成了,是時候被作業系統回收了

等待佇列[2]

  1. 執行緒 1 正在持有物件的鎖,此時執行緒 5 和執行緒 6 由於未獲得鎖而處於阻塞狀態;執行緒 2、執行緒 3、執行緒 4 沒有去奪取鎖,因此處於等待狀態,位於等待佇列中
  2. 執行緒 1 通過呼叫 wait 方法釋放當前持有的鎖,進入等待狀態,當前執行緒 1 在釋放鎖之後進入等待狀態中歐你
  3. 在同步佇列中的執行緒 5 的位置比執行緒 6 要靠前,因此執行緒 5 會首先獲取到物件的鎖(synhronized 是公平鎖)
  4. 執行緒 5 通過呼叫 notifyAll() 方法喚醒在等待佇列中的所有執行緒,使得它們進入同步佇列
  5. 執行緒 5 執行結束,釋放當前物件鎖
  6. 之後由同步佇列中的執行緒競爭鎖,由於作業系統對於執行緒排程的原因,在這種情況下即使 synchronized是公平鎖,但是最終是哪個執行緒獲取到當前的物件鎖依舊是未知的

Deamon 執行緒

Deamon 執行緒也被稱為“守護執行緒”, 這一類執行緒的主要任務是給其它的執行緒提供一些支援性的工作。 JVM

在不存在非 Deamon 執行緒的情況下將會退出,因此 Deamon 執行緒中的任務不一定會被執行

建立一個 Deamon 執行緒的方式如下:

Thread thread = new Thread(() ->{});
thread.setDaemon(true); // 將當前的執行緒設定為 Deamon 執行緒

執行緒的取消與關閉[1]

Java 沒有顯式的方式直接去停止一個正在執行的執行緒,儘管 Thread 類存在 stopsuspend 等方法顯式地終止執行緒,但是這些方法都存在嚴重的缺陷,因此應當避免使用這些方法。

Java 提供了一種中斷的方式來取消一個執行緒,這是一種協作機制,即使一個執行緒向另一個執行緒傳送中斷訊號,從而停止另一個執行緒的工作

如果一個執行緒能夠在某個操作正常完成之前就能夠將其置入 “完成” 狀態,那麼這個執行緒就被稱為是 “可取消的”。

取消一個正在執行的執行緒有以下幾種情況:

  • 使用者取消請求

    使用 JMX 或其它顯式的取消操作

  • 有時間限制的操作

    比如,如果一個服務端程式在 3s 的時間內都沒能做出響應,那麼就丟棄這個請求

  • 錯誤

    如果由於底層的某些硬體限制,導致出現錯誤的情況。如:記憶體已滿、磁碟已滿等

  • 關閉

    當一個程式或者服務關閉之後,必須對正在處理和等待的執行緒執行對應的操作,使得程式能夠正常退出。在這個過程中,某些正在等待的執行緒可能會被取消

執行緒中斷

與執行緒中斷相關的方法如下:

public class Thread {
    // 這個方法會中斷當前執行緒
    public void interrupt(){}
    
    // 該靜態方法將會清除當前執行緒的中斷狀態
    public static boolean interrupted(){}
    
    // 檢測當前的執行緒是否被中斷
    public boolean isInterrupted(){}
}

執行緒中斷是一種協作機制,執行緒可以通過這種機制來通知另一個執行緒,告訴它在合適或者可能的情況下停止當前工作,並轉而執行其它的工作。


在 Java 的 API 規範中,並沒有將中斷與任何取消語義關聯起來,但實際上,如果在取消之外的其它操作中使用中斷,那麼都是不合適的,並且很難支撐起更大的應用


與一般的設定一個 boolean 位來自定義中斷不同,自定義的中斷在某些情況下可能不能按照預期的工作,這是因為:有個阻塞庫的 API 在呼叫時將會導致自定義的取消操作無法執行,從而使得整個操作一直都是阻塞的。在這種情況下,使用執行緒中將能夠解決這一類問題,因為一般的阻塞庫 API 都會檢查當前執行緒是否已經被中斷,從而終止某些操作


執行緒中斷的本質只是設定執行緒的中斷標誌,大部分的的阻塞庫 API 對於中斷的處理如下:清除當前執行緒的中斷標記,然後丟擲 InterruptedException。這是因為:每個新建立的執行緒都不是在自己的執行緒環境下執行的,它只能在父執行緒服務(如執行緒池)中進行,對於中斷的響應應當是通知呼叫者執行自定義的後續處理,而不是由自己處理,這就是為什麼大部分的阻塞庫函式都只是丟擲 InterruptedException 異常的原因


呼叫 interrupt 並不意味著立即停止目標執行緒正在執行的工作,而只是傳遞了請求中斷的訊息


在使用靜態的 interrupted() 方法時要格外小心,因為它會清除當前執行緒的中斷狀態。如果在呼叫 interrupted() 時返回了 true,那麼除非希望遮蔽這個中斷,否則應當再次呼叫 Thread 物件的例項方法 interrupt() 來恢復執行緒的中斷狀態。具體的示例如下:

import java.util.concurrent.*;
// 此程式碼來自 《Java 併發程式設計實戰》
public class TaskRunnable implements Runnable {
    BlockingQueue<Task> queue;

    public void run() {
        try {
            processTask(queue.take());
        } catch (InterruptedException e) {
            /*
            	由於 BlockingQueue 的 take 方法在響應中斷時會清除執行緒的中斷狀態,
            	因此在捕獲到這個異常時需要再次將當前的執行緒的中斷狀態恢復
            */
            Thread.currentThread().interrupt();
        }
    }

    void processTask(Task task) {
    }

    interface Task {
    }
}

響應中斷

一般響應中斷主要有以下兩種方式:

  • 傳遞異常,使得呼叫該方法的方法也變成可中斷的阻塞方法

    BlockingQueue<Task> queue;
    // 丟擲 InterruptedException,使得 getNextTask 成為可中斷的阻塞方法
    public Task getNextTask() throws InterruptedException {
        return queue.take();
    }
    
  • 恢復中斷狀態,使得呼叫棧中的上層程式碼能夠對其進行處理

    如果不想或者無法傳遞 InterruptedException,那麼恢復執行緒的中斷狀態將是一個可選的方案


只有在實現了中斷策略的程式碼才能遮蔽中斷請求,在常規的任務和程式碼庫中都不應該遮蔽中斷請求

執行緒的生命週期

執行緒建立

Java 中建立一個執行緒,最終對應的原始碼如下

private Thread(
    ThreadGroup g, 
    Runnable target, 
    String name,
    long stackSize, 
    AccessControlContext acc,
    boolean inheritThreadLocals
) {
    // 省略一部分引數檢測程式碼。。。。
    this.name = name;
    // 將當前執行緒設定為要建立的執行緒的父執行緒
    Thread parent = currentThread();

    if (g == null) {
        if (g == null) {
            g = parent.getThreadGroup();
        }
    }
    
    g.checkAccess();
    // 省略一部分不太重要的程式碼。。。。

    g.addUnstarted();

    this.group = g;
    // 將 Deamon、priority 屬性設定為父執行緒對應的屬性
    this.daemon = parent.isDaemon();
    this.priority = parent.getPriority();
    
    // 省略一部分不太重要的程式碼
    
    this.inheritedAccessControlContext =
        acc != null ? acc : AccessController.getContext();
    this.target = target;
    setPriority(priority);
    // 將父執行緒相關的 ThrealLocal 物件複製到當前建立的執行緒
    if (inheritThreadLocals && parent.inheritableThreadLocals != null)
        this.inheritableThreadLocals =
        ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
    /* Stash the specified stack size in case the VM cares */
    this.stackSize = stackSize;

    // 設定當前要建立的執行緒的 ID
    this.tid = nextThreadID();
}

執行緒啟動

新建立的執行緒只是一個單獨的物件,如果需要使得作業系統能夠去排程這個執行緒,那麼就需要呼叫執行緒物件的 start() 方法,將當前執行緒的狀態轉換為 Runnbale 狀態

具體的 start() 原始碼為 native 方法,在此不做深入的分析

執行緒執行

一般正常的執行,在上文已經詳細介紹過,在此不做贅述

執行緒終止

可以通過執行緒中斷的方式或者自定義 boolean 標誌位的方式來終止一個執行緒

public class ShutDown {
    public static void main(String[] args) {
        Runner one = new Runner();
        Thread countThread = new Thread(one, "Thread One");
        countThread.start();
        countThread.interrupt(); // 使用執行緒中斷的方式來終止當前執行的執行緒

        Runner two = new Runner();
        countThread = new Thread(two, "Thread Two");
        countThread.start();
        two.cancel(); // 通過自定義 boolean 標誌位的方式來終止一個執行緒
    }

    static class Runner implements Runnable {
        private long i;
        private volatile boolean on = true; // 這個欄位必須是 volatile 修飾的

        public void run() {
            while (on && !Thread.currentThread().isInterrupted())
                i++;
        }

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

執行緒間通訊

JMM

JMM 定義的偏序規則,volatile 和加鎖都可以實現執行緒之間的通訊

volatile 修飾的變數保證了變數的可見性和有序性

synchronized 和其它的加鎖機制保證了執行緒之間的可見性和排它性

等待/通知機制

  • 等待方(消費者)

    1. 獲取物件的鎖
    2. 如果條件不滿足,則呼叫鎖物件的 wait() 方法
    3. 條件滿足則執行對應的邏輯

    虛擬碼形式如下:

    synchronized (lock) {
        while (condition) {
            lock.wait();
        }
        // 對應的處理邏輯
    }
    
  • 通知方(生產者)

    1. 獲取物件的鎖
    2. 改變原有條件
    3. 喚醒所有在等待佇列中的執行緒

    虛擬碼的形式如下:

    synchronized (lock) {
        // 改變等待方的條件
        lock.notifyAll(); // 避免使用 notify,因為它會隨機喚醒一個在等待佇列中的執行緒
    }
    

join 方法

在 JDK 1.7 中的描述如下:[3]

Waiting for the finalization of a thread

In some situations, we will have to wait for the finalization of a thread. For example, we may have a program that will begin initializing the resources it needs before proceeding with the rest of the execution. We can run the initialization tasks as threads and wait for its finalization before continuing with the rest of the program. For this purpose, we can use the join() method of the Thread class. When we call this method using a thread object, it suspends the execution of the calling thread until the object called finishes its execution.

大概意思:主執行緒等待子執行緒的終止。如果在主執行緒的程式碼塊中,遇到了 t.join() ,那麼當前執行的執行緒就需要等待 t 執行完成之後才能繼續執行。

join 方法的本質是通過鎖物件的 wait() 方法來實現的(即 “等待/通知” 機制),對應的原始碼如下:

public final synchronized void join(long millis)
    throws InterruptedException {
    /* 
     isAlive() 用於判斷當前的執行緒是否存活,
     這裡的主要目的是避免由於鎖物件的虛假喚醒帶來的影響
    */
    while (isAlive()) {
        wait(0);
    }
    
    // 省略一部分不太重要的程式碼
}

由於是呼叫鎖物件的 wait() 方法,因此 join() 方法會釋放當前持有的鎖

參考:

[1] 《Java 併發程式設計實戰》

[2] https://blog.csdn.net/pange1991/article/details/53860651

[3] https://www.cnblogs.com/duanxz/p/5038471.html