在Java中無法搶占式地停止一個任務的執行,而是通過中斷機制實現了一種協作式的方式來取消任務的執行。外部程序只能向一個線程發送中斷請求,然后由任務自己負責在某個合適的時刻結束執行。
1. 設置取消標志
這是最基本也是最簡單的停止一個任務執行的辦法,即設置一個取消任務執行的標志變量,然后反復檢測該標志變量的值。
public class MyTask implements Runnable { private volatile running = true; public void run() { while(running) { //...操作 } } public void stop() { running = false; } }
通常需要使用volatile關鍵字來修飾標志變量,以保證該任務類是線程安全的。但是,如果run方法中存在阻塞的操作,則該任務可能永遠也無法正常退出。
2. 中斷線程的執行
每個線程都有一個boolean類型的變量來標志該線程的中斷狀態,Thread類中包含三個與中斷狀態相關的方法:
interrupt方法試圖中斷線程並設置中斷狀態標志變量為true;
isInterrupted方法測試線程是否已經中斷,返回中斷狀態變量的值;
interrupted方法用於清除線程的中斷狀態,並返回之前的值,即如果當前線程是中斷狀態,則重新設置為false,並且返回true;
中斷一個線程常用的辦法即通過調用interrupt方法試圖中斷該線程,線程中執行的任務收到中斷請求后會選擇一個合適的時機結束線程。需要注意的是通常由線程的所有者來從外部中斷線程的執行,因為通常只有線程的所有者知道在滿足某種條件時可以請求中斷線程。
3. 阻塞方法與線程的中斷
阻塞方法會使線程進入阻塞的狀態,例如:等待獲得一個鎖、Thread.sleep方法、BlockingQueue的put、take方法等。
大部分的阻塞方法都是響應中斷的,即這些方法在線程中執行時如果發現線程被中斷,會清除線程的中斷狀態,並拋出InterruptedException表示該方法的執行過程被從外部中斷。響應中斷的阻塞方法通常會在入口處先檢查線程的中斷狀態,線程不是中斷狀態時,才會繼續執行。
根據阻塞方法的特性,我們就可以利用中斷的機制來結束包含阻塞方法的任務的執行:
public MyTask implements Runnable { public void run() { try { while(!Thread.currentThread().isInterrupted()) { Thread.sleep(3000); //阻塞方法 //...其他操作
return; } } catch(InterruptedException ex) { Thread.currentThread().interrupt(); //恢復中斷狀態 } } } public class Test { public void method() { Thread thread = new Thread(new MyTask()).start(); //.... thread.interrupt(); //通過中斷機制請求結束線程的執行 } }
在上面例子中,在線程的任務中包含了阻塞方法sleep,在線程外部通過interrupt方法請求結束線程的執行,sleep方法在檢測到線程處於中斷狀態時,會清除線程的中斷狀態並拋出InterruptedException。對於阻塞方法拋出的InterruptedException,通常有兩種處理方法:
第一種是重新拋出InterruptedException,將該異常的處理權交給方法調用者,這樣該方法也成為了阻塞方法(調用了阻塞方法並且拋出InterruptedException);
第二種是通過interrupt方法恢復線程的中斷狀態,這樣可以使得處理該線程的其他代碼能夠檢測到線程的中斷狀態;
上面的例子采用的是第二種方法,因為阻塞方法是在Runnable接口的run方法中執行的,並沒有其他客戶方法直接調用Runnable的run方法,因此沒有接收InterruptedException的調用者。
對於不支持取消但仍然調用了響應中斷的阻塞方法的任務,應該先在本地保存中斷狀態,然后在任務結束時恢復中斷狀態,而不是在捕獲InterrruptedException時就恢復中斷狀態:
public class MyTask implements Runnable { boolean interrupted = false; public void run() { try { while(true) //不支持取消操作 { try { Thread.sleep(3000); //...其他操作
return; } catch(InterruptedException ex) { interrupted = true; //在本地保存中斷狀態
//Thread.currentThread().interrupt(); //不要在這兒立即恢復中斷 } } } finally { if(interrupted) Thread.currentThread().interrupt(); //恢復中斷 } } }
在上面的例子中,由於大部分響應中斷阻塞方法都會在方法的入口處檢查線程的中斷狀態,如果在捕獲InterruptedException的地方立即恢復中斷,則可能導致剛恢復的中斷狀態被阻塞方法的入口處被檢測到,從而又再次拋出InterruptedException,這樣可能導致程序陷入死循環。
也有一些阻塞方法是不響應中斷的,即在收到中斷請求時不會拋出InterruptedException,如:java.io包中的Socket I/O方法、java.nio.channels包中的InterruptibleChannel類的相關阻塞方法、java.nio.channels包中的Selector類的select方法等。
如果線程執行的任務中包含這類不響應中斷的方法,則無法通過標准的中斷機制來結束任務的運行,但仍然有其他辦法。如:
對於java.io包的Socket I/O方法,可以通過關閉套接字,從而使得read或者write方法拋出SocketException而跳出阻塞;
java.nio.channels包中的InterruptibleChannel類的方法其實是響應線程的interrupt方法的,只是拋出的不是InterruptedException,而是ClosedByInterruptedException,除此之外,也可以通過調用InterruptibleChannel的close方法來使線程跳出阻塞方法,並拋出AsynchronousClosedException;
對於java.nio.channels包的Selector類的select方法,可以通過調用Selector類的close方法或者wakeup方法從而拋出ClosedSelectorExeception;
可以通過改寫Thread類的interrupt方法從而將非標准的中斷線程的機制封裝在Thread中,以中斷包含Socket I/O的任務為例:
public class ReadThread extends Thread { private final Socket client; private final InputStream in; public ReadThread(Socket client) throws IOException { this.client = client; in = client.getInputStream(); } public void interrupt() { try { socket.close(); } catch(IOException ignore){} finally { super.interrupt(); } } public void run() { //調用in.read方法 } }
4. 通過Future來取消任務的執行
Future接口有一個cancel方法,可以通過該方法取消任務的執行,cancel方法有一個boolean型的參數mayInterruptIfRunning。
如果設置為false,對於正在執行的任務只能等到任務執行完畢,無法中斷;
如果設置為true,對於正在執行的任務可以試圖中斷任務的運行,這種情況通常只在與Executor框架配合時使用,因為執行任務的線程是由Executor創建的,Executor知道該如何中斷執行任務的線程;
puhblic class Test { private Executor executor = Executors.newSingleThreadExecutor(); public static void timedRun(Runnable runnable,long timeout,TimeUnit unit) throws InterruptedException { try { Future<?> task = executor.submit(runnable); task.get(timeout,unit); //任務最多運行指定的時間 } catch(TimeoutException e1){} catch(ExecutionException e2) { throw e2.getCause(); } finally { task.cancel(true); //取消任務的執行 } } }
參考資料 《Java並發編程實戰》