Java Thread.interrupt 複習

本文轉載自:Java Thread.interrupt[點擊打開鏈接], 文章寫的太好,怕丟了,所以原文也一併拷貝過來。

下面這個場景你可能很熟悉,我們調用Thread.sleep(),condition.await(),但是IDE提示我們有未捕獲的InterruptedException。什麼是InterruptedException呢?我們又應該怎麼處理呢?

大部分人的回答是,吞掉這個異常就好啦。但是其實,這個異常往往帶有重要的信息,可以讓我們具備關閉應用時執行當前代碼回收的能力。

Blocking Method

如果一個方法拋出InterruptedException(或者類似的),那麼說明這個方法是一個阻塞方法。(非阻塞的方法需要你自己判斷,阻塞方法只有通過異常才能反饋出這個東西)

當我們直接調用一個Unschedule的線程的interrupt方法的時候,會立即使得其變成schedule(這一點非常重要,由JVM保證), 並且interrupted狀態位爲true。

通常low-level method,像sleep和await這些方法就會在方法內部處理這個標誌位。例如await就會在醒來之後檢查是否有中斷。所以在Sync內部通常在喚醒之後都會檢查中斷標誌位。

看下面一段代碼:

  public static void main(String[] args) {
        Thread a = new Thread(new Runnable() {
            @Override
            public void run() {
                long start = System.currentTimeMillis();
                try {
                    Thread.sleep(10000);
                } catch (InterruptedException e) {
                    // what to do ?
                }
                System.out.println(System.currentTimeMillis() - start);
            }
        });
        a.start();
        // 加上這句話,執行時間是0,沒有這句話執行時間是10,你感受下
        a.interrupt(); 
}

所以,當我們直接Interrupt一個線程的時候,他會立即變成可調度的狀態,也就是會里面從阻塞函數中返回。這個時候我們拿到InterruptedException(或者在更底層看來只是一個線程中斷標誌位)的時候應該怎麼做呢?

在low-level的層面來說,只有中斷標誌位,這一個概念,並沒有interruptException,只是jdk的框架代碼中,爲了強制讓客戶端處理這種異常,所以在同步器、線程等阻塞方法中喚醒後自動檢測了中斷標誌位,如果符合條件,則直接拋出受檢異常。

How to Deal

Propagating InterruptedException to callers by not catching it

當你的方法調用一個blocking方法的時候,說明你這個方法也是一個blocking方法(大多數情況下)。這個時候你就需要對interruptException有一定的處理策略,通常情況下最簡單的策略是把他拋出去。參考下面的代碼:

參考blockingQueue的寫法,底層使用condition對象,當await喚醒的時候有interruptException的時候,直接拋出,便於上層處理。換句話說,你的代碼這個層面沒有處理的必要和意義。

public class TaskQueue {
    private static final int MAX_TASKS = 1000;

    private BlockingQueue<Task> queue 
        = new LinkedBlockingQueue<Task>(MAX_TASKS);

    public void putTask(Task r) throws InterruptedException { 
        queue.put(r);
    }

    public Task getTask() throws InterruptedException { 
        return queue.take();
    }
}

Do some clean up before thrown out

有時候,在當前的代碼層級上,拋出interruptException需要清理當前的類,清理完成後再把異常拋出去。下面的代碼,表現的就是一個遊戲匹配器的功能,首先等待玩家1,玩家2都到達之後開始遊戲。如果當前玩家1到達了,線程接受到interrupt請求,那麼釋放玩家1,這樣就不會有玩家丟失。

public class PlayerMatcher {
    private PlayerSource players;

    public PlayerMatcher(PlayerSource players) { 
        this.players = players; 
    }

    public void matchPlayers() throws InterruptedException { 
        Player playerOne, playerTwo;
         try {
             while (true) {
                 playerOne = playerTwo = null;
                 // Wait for two players to arrive and start a new game
                 playerOne = players.waitForPlayer(); // could throw IE
                 playerTwo = players.waitForPlayer(); // could throw IE
                 startNewGame(playerOne, playerTwo);
             }
         }
         catch (InterruptedException e) {  
             // If we got one player and were interrupted, put that player back
             if (playerOne != null)
                 players.addFirst(playerOne);
             // Then propagate the exception
             throw e;
         }
    }
}

Resorting Status

如果已經到了拋不出去的地步了,比如在Runnable中。當一個blocking-method拋出一個interruptException的時候,當前線程的中斷標誌位實際是已經被清除了的,如果我們這個時候不能再次拋出interruptException,我們就無法向上層表達中斷的意義。這個時候只有重置中斷狀態。但是,這裏面還是有很多技巧...不要瞎搞:

public class TaskRunner implements Runnable {
    private BlockingQueue<Task> queue;
    public TaskRunner(BlockingQueue<Task> queue) { 
        this.queue = queue; 
    }
    public void run() { 
        try {
             while (true) {
                 Task task = queue.take(10, TimeUnit.SECONDS);
                 task.execute();
             }
         } catch (InterruptedException e) { 
             // Restore the interrupted status
             Thread.currentThread().interrupt();
         }
    }
}

注意上面代碼,catch異常的位置,在看下面一段代碼

public class TaskRunner implements Runnable {
    private BlockingQueue<Task> queue;
    public TaskRunner(BlockingQueue<Task> queue) { 
        this.queue = queue; 
    }
    public void run() { 
       while (true) {
            try {
              Task task = queue.take(10, TimeUnit.SECONDS); 
              task.execute(); 
            } catch (InterruptedException e) { 
              Thread.currentThread().interrupt();
            }
        }
}

這段代碼就會造成無限循環,catch住之後,設置中斷標誌,然後loop,take()函數立即拋出InterruptException。你感受下。

千萬不要直接吞掉

當你不能拋出InterruptedException,不論你決定是否響應interrupt request,這個時候你都必須重置當前線程的interrupt標誌位,因爲interrupt標誌位不是給你一個人看的,還有很多邏輯相應這個狀態。標準的線程池(ThreadPoolExecutor)的Worker對象(內部類)其實也會對interrupt標識位響應,所以向一個task發出中斷信號又兩個作用,1是取消這個任務,2是告訴執行的Thread線程池正在關閉。如果一個task吞掉了中斷請求,worker thread就不能響應中斷請求,這可能導致application一直不能shutdown.

萬不要直接吞掉
// Don't do this 
public class TaskRunner implements Runnable {
    private BlockingQueue<Task> queue;
    public TaskRunner(BlockingQueue<Task> queue) { 
        this.queue = queue; 
    }
    public void run() { 
        try {
             while (true) {
                 Task task = queue.take(10, TimeUnit.SECONDS);
                 task.execute();
             }
         }
         catch (InterruptedException swallowed) { 
             /* DON'T DO THIS - RESTORE THE INTERRUPTED STATUS INSTEAD */
         }
    }
}

Implementing cancelable tasks

從來沒有任何文檔給出interruption明確的語義,但是其實在大型程序中,中斷可能只有一個語義:取消,因爲別的語義實在是難以維持。舉個例子,一個用戶可以用通過GUI程序,或者通過一些網絡機制例如JMX或者WebService來發出一個關閉請求。也可能是一段程序邏輯,再舉個簡單的例子,一個爬蟲程序如果檢測到磁盤滿了,可能就會自行發出中斷(取消)請求。或者一個並行算法可能會打開多個線程來搜索什麼東西,當某個框架搜索到結果之後,就會發出中斷(取消)請求。

一個task is cancelable並不意味着他必須立刻響應中斷請求。如果一個task在loop中執行,一個典型的寫法是在每次Loop中都檢查中斷標誌位。可能循環時間會對響應時間造成一定的delay。你可以通過一些寫法來提高中斷的響應速度,例如blocking method裏面往往第一行都是檢查中斷標誌位。

Interrupts can be swallowed if you know the thread is about to exit 唯一可以吞掉interruptException的場景是,你明確知道線程就要退出了。這個場景往往出現在,調用中斷方法是在你的類的內部,例如下面一段代碼,而不是被某種框架中斷。

public class PrimeProducer extends Thread {
    private final BlockingQueue<BigInteger> queue;
    PrimeProducer(BlockingQueue<BigInteger> queue) {
        this.queue = queue;
    }
    public void run() {
        try {
            BigInteger p = BigInteger.ONE;
            while (!Thread.currentThread().isInterrupted())
                queue.put(p = p.nextProbablePrime());
        } catch (InterruptedException consumed) {
            /* Allow thread to exit */
        }
    }
    public void cancel() { interrupt(); }
}

Non-Interruptible Blocking

並不是所有blockingMethod都支持中斷。例如input/outputStream這兩個類,他們就不會拋出InterruptedException,也不會因爲中斷而直接返回。在Socket I/O而言,如果線程A關閉了Socket,那麼正在socket上讀寫數據的B、C、D都會拋出SocketException. 非阻塞I/O(java.nio)也不支持interruptiable I/O,但是blocking operation可以通過關閉channel或者調用selector.wakeUp方法來操作。類似的是,內部鎖(Synchronized Block)也不能被中斷,但是ReentrantLock是支持可被中斷模式的。

Non-Cancelable Tasks

有一些task設計出來就是不接受中斷請求,但是即便如此,這些task也需要restore中斷狀態,以便higher-level的程序能夠在這個task執行完畢後響應中斷請求。

下面這段代碼就是一個BlockingQueue.poll()忽略中斷的的例子(和上面我的備註一樣,不要在catch裏面直接restore狀態,不然queue.take()會造成無限循環。

public Task getNextTask(BlockingQueue<Task> queue) {
    boolean interrupted = false;
    try {
        while (true) {
            try {
                return queue.take();
            } catch (InterruptedException e) {
                interrupted = true;
                // fall through and retry
            }
        }
    } finally {
        if (interrupted)
            Thread.currentThread().interrupt();
    }
}

Summary

你可以利用interruption mechanism來提供一個靈活的取消策略。任務可以自行決定他們是否可以取消,或者如何響應中斷請求,或者處理一些task relative cleanup。即便你想要忽略中斷請求,你也需要restore中斷狀態,當你catchInterruptedException的時候,當higher不認識他的時候,就不要拋出啦。

如果你的代碼在框架(包括JDK框架)中運行,那麼interruptException你就務必像上面一樣處理。如果單純的你自己的小代碼片段,那麼你可以簡單地認爲InterruptException就是個bug。

在生產環境下,tomcat shutdown的時候,也會大量關閉線程池,發出中斷請求。這個時候如果響應時間過於慢就會導致tomcat shutdown非常的慢(甚至於不響應)。所以大部分公司的重啓腳本中都含有重啓超時(例如20s)的一個強殺(kill -9)的兜底策略,這個過程對於程序來說就等於物理上的斷電,凡是不可重試,沒有斷電保護,業務不冪等的情況都會產生大量的數據錯誤。

就現在業內的做法而言,大部分上述描述的問題幾乎已經不再通過interrupt這種關閉策略來解決(因爲實在難以解決),轉而通過整體的系統架構模型來規避數據問題,例如數據庫事務,例如可重試的冪等任務等等。

針對前端用戶而言,就是ng的上下線心跳切換。但即使如此,對於請求已經進入tomcat線程池中的前端用戶而言,還是會存在極其少量的服務器繁忙:)

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