Java 線程的中斷機制 原 薦

今天我們聊聊 Java 線程的中斷機制。

線程中斷機制提供了一種方法,有兩種常見用途:

  1. 將線程從阻塞等待中喚醒,並作出相應的“受控中斷”處理。
  2. 嘗試告知目標線程:請打斷現有處理流程,響應新的命令。

以第一種用途爲例,請看以下代碼:

synchronized (lock) {
    try {
        while (!check()) {
            lock.wait(1000);
        }
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

這段代碼使用了 Java 提供的 wait/notify 機制,線程執行 lock.wait() 會阻塞,有三種情況使線程恢復運行。

  1. 超時 1000ms 結束,正常執行下一句代碼。

  2. 另一個線程執行下述代碼主動喚醒

    synchronized (lock) {
        lock.notifyAll(); // or lock.notify();
    }
    

    這也會正常執行下一句代碼。

  3. 另一個線程要求等待的線程“中斷”

    // 拿到等待中的線程的引用
    Thread a;
    a.interrupt();
    

    被“中斷”的線程 a,會在 lock.wait() 處拋出 InterruptedException 異常。

綜上所述,你可以認爲 object.wait() 內部在做這些事:

boolean checkTimeout = timeout > 0;
Thread current = Thread.currentThread();
lock.addWaiter(current);
while (!current.isNotified()) {
    if (current.isInterrupted()) {
        current.clearInterrupted();
        throw new InterruptedException();
    }
    if (checkTimeout) {
        if (timeout == 0) break;
        timeout--;
    }
}

這不完全準確,因爲 wait 不使用這種“忙輪詢”的方式做檢查,但關於標誌位的判斷邏輯是正確的。

讓我們從上文所述的“手動發出中斷”這一操作開始探究,

// sun.nio.ch.Interruptible

public interface Interruptible {
    void interrupt(Thread var1);
}

// java.lang.Thread

private volatile Interruptible blocker;
private final Object blockerLock = new Object();

public void interrupt() {
    if (this != Thread.currentThread())
        checkAccess();

    synchronized (blockerLock) {
        Interruptible b = blocker;
        if (b != null) {
            interrupt0();
            b.interrupt(this);
            return;
        }
    }
    interrupt0();
}

// Just to set the interrupt flag
private native void interrupt0();

能夠看出,thread.interrupt() 先判斷權限,然後實際調用 interrupt0() 設置線程的中斷標誌,如果當前線程有 nio 的 Interruptible 那麼還會回調它。

注意,interrupt0() 只是設置了線程的中斷標誌。

當一個線程並不阻塞,沒有在 object.wait(), thread.join(), Thread.sleep() 等不受 Java 程序邏輯控制的區域時,那麼會發生什麼事情?答案是不會發生任何事情,線程是否被打斷只能通過主動地檢查中斷標誌得知。

怎麼檢查?Thread 暴露了兩個接口,Thread.interrupted()thread.isInterrupted()

// java.lang.Thread

public static boolean interrupted() {
    return currentThread().isInterrupted(true);
}

public boolean isInterrupted() {
    return isInterrupted(false);
}

private native boolean isInterrupted(boolean clearInterrupted);

能夠看出,兩者都是依靠內部的 isInterrupted(boolean),而它會返回線程是否被打斷,並根據需要清空中斷標誌。

當一個函數調用會發生阻塞,Java 庫函數在阻塞的源頭簽名裏標記 throws InterruptedException,並要求編寫 try catch 處理中斷。

當線程發生了阻塞,就像上文所述,Java 檢查到中斷標誌,先將其清除,然後拋出 InterruptedException

// java.lang.Object

public final void wait() throws InterruptedException {
    wait(0);
}

public final native void wait(long timeout) throws InterruptedException;

如果一個線程收到 InterruptedException,之後仍然執行了會引發阻塞的代碼,它將像“沒事人”一樣繼續阻塞住。因爲 Java 在內部將中斷標誌清除了!

我們常見地編寫以下三類處理 InterruptedException 的代碼:

InterruptedException 交由上層處理。

public void foo() throws InterruptedException {
    synchronized (lock) {
        lock.wait();
    }
}

遇到 InterruptedException 重設中斷標誌位。

try {
    synchronized (lock) {  
        lock.wait();  
    }  
} catch (InterruptedException e) {  
    Thread.currentThread().interrupt();
    //break; 
} 

先忙完,再重新拋出 InterruptedException

public void bar() throws InterruptedException {
    InterruptedException ie = null;
    boolean done = false;
    while (!done) {
        synchronized (lock) {
            try {
                lock.wait();
            } catch (InterruptedException e) {
                ie = e;
                continue;
            }
        }
        done = true;
    }
    if (ie != null) {
        throw ie;
    }
}

如果一個線程無視中斷標誌和 InterruptedException,它仍然能夠跑的很好。但這與我們設計多線程的初衷是違背的,我們希望線程之間是和諧的有序協作以實現特定功能,因此受控線程應當對中斷作出響應。而 Java 留給開發者這一自由,我們應當予以善用。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章