線程終止方法

任務和線程的啓動都很容易, 大多數時候, 線程會運行至終止, 然而有些時候, 我們會提前終止線程的運行, 比如:
1. 用戶取消請求,: 圖形界面操作或者 JMX 請求
2. 有時間限制的操作
3. 應用程序事件: 多個任務進行搜索, 其中一個得到結果後, 其他的應該終止
4. 錯誤和程序關閉

然而 JVM 並沒有提供任何機制來安全的終止線程, Java 中已經廢棄的 Thread 類的 stop, suspend 方法可以快速的終止或停止一個線程, 但是 stop 會導致共享數據不一致, suspend 不釋放鎖, 會導致線程死鎖的問題. Oracle 官方的文章: 爲什麼廢棄 Thread.stop, Thread.suspend,Thread.resume
在大多數時候, 我們並不應該立即停止線程, 而是需要一種協作機制, 當線程收到終止信號時, 它會首先清除當前正在執行的工作, 然後再退出. Java 提供了 interrupt, 這是一種協作機制, 提供了一種協作方式, 來達到線程終結的目的.
協作式可取消的任務 (Cancellable Task) 取消策略需要思考這樣幾個問題, How, When, What, 即外界代碼如何 (How) 取消任務執行, 任務代碼何時 (When) 檢查任務已經取消, 取消任務時應該執行哪些 (What) 清理操作.

我們來分析一下幾種線程協作, 停止的方法.

使用 volatile 類型的域來保存取消狀態

class Task implements Runnable {
    private volatile boolean cancelled = false;

    @Override
    public void run() {
        while (!cancelled) {
            doSomething();
        }
    }

    private void doSomething() {
        ....
    }

    public void cancel() {
        cancelled = true;
    }
}

最普通的一種協作方式, 就是自己使用 volatile boolean 類型變量來保存外界使用想要終止這個線程. 在 while 邊界中檢查是否已經設置停止信號. 如果在 doSomethingz 方法中調用了會阻塞的方法, 比如 BlockingQueue.put() 或者 Object.wait(), InputStream.read() 等方法, 那麼線程可能永遠都不會喚醒去執行取消標誌, 也就不會結束.

會阻塞的 method 在這裏我把它分成兩種, 一種是拋出 InterruptedException 的可中斷的阻塞 method, 一種是和同步阻塞 IO 相關, 不可中斷的阻塞 method.
1. Object.wait, Thread.join, Thread.sleep, BlockingQueue.put, BlockingQueue.take 等拋出 InterruptedException 異常的 method
2. InputStream.read, OutputStream.write 等和同步阻塞 IO 相關的 method

可中斷的阻塞方法調用

對於上面的第一類拋出InterruptedException的阻塞方法, 可以使用被阻塞的線程的interrupt方法進行喚醒. 每個線程都有一個boolean類型的中斷狀態.這些方法調用前或者阻塞時都會檢測中斷標誌是否設置, 或者清除中斷狀態, 或者返回, 或者拋出異常.
thread.interrupt()方法會設置一箇中斷標誌.
Thread.currentThread().isInterrupted():返回線程是否中斷, 不清除中斷標誌位,調用的是Thread.currentThread().isInterrupted(false).
Thread.interrupted():返回線程是否中斷, 清除中斷標誌位.其實底層調用的是Thread.currentThread().isInterrupted(true).

class InterruptableTask extends Thread {
    private BlockingQueue<String> queue;

    @Override
    public void run() {
        doSomething(queue);
    }

    private void doSomething(BlockingQueue<String> queue) {
        try {
            //每取完一次後, 都檢查一下是否中斷, 如果中斷則退出執行. Thread.currentThread.isInterrupted()方法會檢查以及不清除中斷標誌位
            while (!Thread.currentThread.isInterrupted()) {
                consume(queue.take());
            }
        } catch (InterruptedException e) {
            //在阻塞時, 檢查到中斷後, 打印出中斷異常, 退出執行
            e.printStackTrace();
        }
    }

    private void consume(String take) {
        System.out.println(take);
    }

    public BlockingQueue<String> getQueue() {
        return queue;
    }

    public InterruptableTask setQueue(BlockingQueue<String> queue) {
        this.queue = queue;
        return this;
    }

    public void cancel() {
        interrupt();
    }
}

當檢查到中斷請求時, 任務並不需要立即放棄所有的操作, 它可以推遲處理中斷請求, 並直到某個更合適的時刻, 因此它需要記住中斷請求, 並在完成當前任務後拋出 InterruptedException 或者標識已收到中斷請求.

同步阻塞的方法調用

對於不可中斷的同步阻塞 IO 方法調用, 如果需要中斷線程, 對於 socket 和 InputStream,OutputStream 等方法, 可以調用 close() 方法, 如果線程在調用同步阻塞方法時被阻塞, close 方法會使得線程喚醒, 並拋出異常.

class CancelableIOTask extends Thread {
    private InputStream in;

    @Override
    public void run() {
        try {
            doSomething(in);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private void doSomething(InputStream in) throws IOException {
        try {
            BufferedReader reader = new BufferedReader(new InputStreamReader(in));
            while (true) {
                consume(reader.readLine());
            }
        } finally {
            in.close();
        }
    }

    private void consume(String take) {
        System.out.println(take);
    }


    public void cancel() throws IOException {
        in.close();
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章