併發編程實戰學習筆記(五)——取消與關閉

題記

在Java中沒有一種安全的搶佔方法來停止線程,因此也就沒有安全的搶佔式方法來停止任務。只有一些協作式的機制,使請求取消的任務和代碼都遵循一種協商好的協議。

響應中斷時執行的操作包括

  • 清除中斷狀態
  • 拋出InterrruptedException,表示阻塞操作由於中斷而提前結束

對中斷操作的正確理解

調用interrupt並不意味着立即停止目標線程正在進行的工作,而只是傳遞了請求中斷的消息。
它並不會真正地中斷一個正在運行的線程,而只是發出中斷請求,然後由線程在下一合適的時刻中斷自己。

論斷

  • 通常,中斷是實現取消的最合理方式。
  • 只有實現了線程中斷策略的代碼纔可以屏蔽中斷請求。在常規的任務和庫代碼中都不應該屏蔽中斷請求。
  • 使用線程池往往比直接new Thread會更加方便,因爲線程池封裝了相關特性,如線程封裝,批量中斷機制等。

代碼響應方式

  • 阻塞庫方法,捕捉InterruptedException,做好清除工作,然後結束線程
  • 自己的代碼在合適的地方判斷isInterrupted(),以響應中斷
  • 一些阻塞方法但不拋出InterruptedException的,需要具體討論,參見下述

處理不可中斷的阻塞

  • Java.io包中的同步socket I/O。通過關閉底層的套接字,可以使得由於執行read或者write等方法而被阻塞的線程拋出一個SocketException。
  • Java.io包中的同步I/O。當中斷一個正在InterruptibleChannel上等待的線程時,將拋出CloseByInterruptException並關閉鏈路
  • Selector的異步I/O。如果一個線程在調用Selector.select方法(java.io.channels中)時阻塞了,那麼調用close或者wakeup方法會使線程拋出ClosedSelectorException並提前返回。
  • 獲取一個鎖。如果一個線程由於等待某個內置鎖而阻塞,那麼將無法響應中斷。但在,在Lock類中提供了lockInterruptibly方法,該方法允許在等待一個鎖時仍能響應中斷。

區分任務與線程對中斷的反應是很重要的

你是中斷任務還是中斷線程,含義並不相同。比如在線程池裏,你只是中斷任務,處理完成異常,清除狀態之後,只需要簡單返回就行,但如果確定是要中斷線程,則要進行其它考慮了。

響應中斷

當調用可中斷的阻塞函數時,有兩種實用策略可用於處理InterruptedException

  • 傳遞異常(可能在執行某個特定於任務的清除操作之後),從而使得你的方法也成爲可中斷的阻塞方法。
  • 恢復中斷狀態,從而使得調用棧中的上層代碼能夠對其處理。
    注意:只有實現了線程中斷策略的代碼纔可以屏蔽中斷請求。在常規的任務和庫代碼中都不應該屏蔽中斷請求。

線程池中的任務通過Future來實現取消,其中CANCEL API文檔說明

  • 返回結果:返回FALSE:任務已經取消,已經完成等不能取消的原因。否則返回true
  • 如果任務未開始執行,則不再執行
  • 如果任務已經在運行了,則取決於參數mayInterruptIfRunning,true則會被中斷,false則不會中斷

線程池shutdownnow的侷限性:我們無法通過常規方法找出哪些任務已經開始但尚未結束

如果我們需要知道這些任務,並且不想直接中斷而是進行繼續的後續處理,則可以使用TrackingExecutor找出哪些任務已經開始但還沒有正常完成。在Executor結束後,getCancelledTasks返回被取消的任務清單。

public class TrackingExecutor extends AbstractExecutorService{
    private final ExecutorService exec;
    private final Set<Runnable> tasksCancelledAtShutdown = Collections.synchronizedSet(new HashSet<Runnable>());
    ....
    public List<Runnable> getCancelledTasks(){
        if(!exec.isTerminated()) throw new IllegalStateException(....);
        return new ArrayList<Runnable>(tasksCancelledAtShutdown);
    }

    public void executor(final Runnable runnable){
        //重點看這一段代碼
        try{
            runnable.run();
        }finally{
            if(isShutdown() && Thread.currentThread().isInterrupted())
               tasksCancelledAtShutdown.add(runnable);
        }
    }
    // 將ExecutorService其它方法委託給exec
}

//另一段使用TrackingExecutor類的代碼
public synchronized void stop() throws InterruptedException{
    try{
        saveUncrawled(exec.shutdownNow());
        if(exec.awaitTermination(TIMEOUT,UNIT)) 
            saveUncrawled(exec.getCancelledTasks);
    }finally{
        exec = null;
    }
}

非正常的線程終止處理辦法

  • 線程應該在try-catch塊中調用這些任務,這樣就能捕獲那些未檢查的異常了。或者也可以使用try-finally代碼塊來確保框架能夠知道線程非正常退出的情況,並做出正確的響應。你或者會捕獲RuntimeException異常,即當通過Runnable這樣的抽象機制來調用未知和不可信的代碼時。
  • 在Thread API中同樣提供了UncaughtExceptionHandler,它能檢測出某個線程由於未捕獲的異常而終結的情況。
    • 要爲線程池中的所有線程設置一個UncaughtExceptionHandler,需要爲ThreadPoolExecutor的構造函數提供一個ThreadFactory.
    • 令人困惑的是,只有通過execute提交的任務,才能將它拋出的異常交給未捕捉異常處理器,而通過submit提交的任務,無論是拋出的未檢查異常還是已檢查異常,都將被認爲是任務返回狀態的一部分。如果一個由submit提交的任務由於拋出了異常而結束,那麼這個異常將被Future.get封裝在ExecutionException中重新拋出。

QUESTION:主線程退出,子線程沒有退出,JVM會退出嗎?

ANSWER:不會退出,因爲只要有非daemon線程未退出,JVM就不會退出。

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