[懷舊并發01]如何正確結束Java線程

線程的啟動很簡單,但用戶可能隨時取消任務,怎么樣讓跑起來的線程正確地結束,這是今天要討論的話題。

使用標志位

很簡單地設置一個標志位,名稱就叫做isCancelled。啟動線程后,定期檢查這個標志位。如果isCancelled=true,那么線程就馬上結束。

public class MyThread implements Runnable{
    private volatile boolean isCancelled;
    
    public void run(){
        while(!isCancelled){
            //do something
        }
    }
    
    public void cancel(){   isCancelled=true;    }
}

注意的是,isCancelled需要為volatile,保證線程讀取時isCancelled是最新數據。

我以前經常用這種簡單方法,在大多時候也很有效,但并不完善??紤]下,如果線程執行的方法被阻塞,那么如何執行isCancelled的檢查呢?線程有可能永遠不會去檢查標志位,也就卡住了。

使用中斷

Java提供了中斷機制,Thread類下有三個重要方法。

  • public void interrupt()
  • public boolean isInterrupted()
  • public static boolean interrupted(); // 清除中斷標志,并返回原狀態

每個線程都有個boolean類型的中斷狀態。當使用Thread的interrupt()方法時,線程的中斷狀態會被設置為true。

下面的例子啟動了一個線程,循環執行打印一些信息。使用isInterrupted()方法判斷線程是否被中斷,如果是就結束線程。

public class InterruptedExample {

    public static void main(String[] args) throws Exception {
        InterruptedExample interruptedExample = new InterruptedExample();
        interruptedExample.start();
    }

    public void start() {
        MyThread myThread = new MyThread();
        myThread.start();

        try {
            Thread.sleep(3000);
            myThread.cancel();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    private class MyThread extends Thread{

        @Override
        public void run() {
            while (!Thread.currentThread().isInterrupted()) {
                try {
                    System.out.println("test");
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    System.out.println("interrupt");
                    //拋出InterruptedException后中斷標志被清除,標準做法是再次調用interrupt恢復中斷
                    Thread.currentThread().interrupt();
                }
            }
            System.out.println("stop");
        }

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

對線程調用interrupt()方法,不會真正中斷正在運行的線程,只是發出一個請求,由線程在合適時候結束自己。

例如Thread.sleep這個阻塞方法,接收到中斷請求,會拋出InterruptedException,讓上層代碼處理。這個時候,你可以什么都不做,但等于吞掉了中斷。因為拋出InterruptedException后,中斷標記會被重新設置為false!看sleep()的注釋,也強調了這點。

@throws  InterruptedException
     if any thread has interrupted the current thread. 
     The interrupted status of the current thread is 
     cleared when this exception is thrown.
public static native void sleep(long millis) throws InterruptedException;

記得這個規則:什么時候都不應該吞掉中斷!每個線程都應該有合適的方法響應中斷!

所以在InterruptedExample例子里,在接收到中斷請求時,標準做法是執行Thread.currentThread().interrupt()恢復中斷,讓線程退出。

從另一方面談起,你不能吞掉中斷,也不能中斷你不熟悉的線程。如果線程沒有響應中斷的方法,你無論調用多少次interrupt()方法,也像泥牛入海。

用Java庫的方法比自己寫的要好

自己手動調用interrupt()方法來中斷程序,OK。但是Java庫提供了一些類來實現中斷,更好更強大。

Executor框架提供了Java線程池的能力,ExecutorService擴展了Executor,提供了管理線程生命周期的關鍵能力。其中,ExecutorService.submit返回了Future對象來描述一個線程任務,它有一個cancel()方法。

下面的例子擴展了上面的InterruptedExample,要求線程在限定時間內得到結果,否則觸發超時停止。

public class InterruptByFuture {

    public static void main(String[] args) throws Exception {
        ExecutorService es = Executors.newSingleThreadExecutor();
        Future<?> task = es.submit(new MyThread());

        try {
            //限定時間獲取結果
            task.get(5, TimeUnit.SECONDS);
        } catch (TimeoutException e) {
            //超時觸發線程中止
            System.out.println("thread over time");
        } catch (ExecutionException e) {
            throw e;
        } finally {
            boolean mayInterruptIfRunning = true;
            task.cancel(mayInterruptIfRunning);
        }
    }

    private static class MyThread extends Thread {

        @Override
        public void run() {
            while (!Thread.currentThread().isInterrupted()) {   
                try {
                    System.out.println("count");
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    System.out.println("interrupt");
                    Thread.currentThread().interrupt();
                }
            }
            System.out.println("thread stop");
        }

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

Future的get方法可以傳入時間,如果限定時間內沒有得到結果,將會拋出TimeoutException。此時,可以調用Future的cancel()方法,對任務所在線程發出中斷請求。

cancel()有個參數mayInterruptIfRunning,表示任務是否能夠接收到中斷。

  • mayInterruptIfRunning=true時,任務如果在某個線程中運行,那么這個線程能夠被中斷;
  • mayInterruptIfRunning=false時,任務如果還未啟動,就不要運行它,應用于不處理中斷的任務

要注意,mayInterruptIfRunning=true表示線程能接收中斷,但線程是否實現了中斷不得而知。線程要正確響應中斷,才能真正被cancel。

線程池的shutdownNow()會嘗試停止池內所有在執行的線程,原理也是發出中斷請求。對于線程池的停止,下次新開一篇再講吧。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 一、并發 進程:每個進程都擁有自己的一套變量 線程:線程之間共享數據 1.線程 Java中為多線程任務提供了很多的...
    SeanMa閱讀 2,537評論 0 11
  • Java-Review-Note——4.多線程 標簽: JavaStudy PS:本來是分開三篇的,后來想想還是整...
    coder_pig閱讀 1,682評論 2 17
  • 線程概述 線程與進程 進程 ?每個運行中的任務(通常是程序)就是一個進程。當一個程序進入內存運行時,即變成了一個進...
    閩越布衣閱讀 1,020評論 1 7
  • java多線程 [TOC] 創建線程 直接調用Thread類或Runnable類的run方法并不會 創建線程,只會...
    蕩輕風閱讀 503評論 0 0
  • 假期我不小心把腰給閃了。只是彎腰撿沙發上的小熊,就咔嚓一聲,腰不能動。接下來整個人都不好了,感覺一下跨躍到老年,腰...
    趙趙的不晚主義閱讀 895評論 0 3