Java多線程學習之線程的取消與中斷機制


  任務和線程的啟動很容易。在大多數情況下我們都會讓他們運行直到結束,或是讓他們自行停止。但是,有時我們希望提前結束任務或是線程,可能是因為用戶請求取消,或是線程在規定時間內沒有結束,或是出現了一些問題迫使線程要提前結束。

  強制一個線程或是服務立即停止,可能會造成共享數據狀態不一致的問題,比如,兩個線程正對一個共享數據進行操作,然后被突然殺死,這樣會對數據造成不確定性的影響。Java中沒有提供任何機制來安全的終止線程,但它提供了中斷,這種協作機制,“提醒”線程可以自己結束自己線程。這種機制提供了更好的靈活性,因為任務本身的代碼比發出取消請求的代碼更清楚如何執行停止工作。

1、使用“標志”變量取消任務

 1 public class PrimeGenerator implements Runnable {
 2     private final List<BigInteger> primes = new ArrayList<>();
 3     // 標志變量,設置為volatile,保證可見性
 4     private volatile boolean canceled = false;
 5     @Override
 6     public void run() {
 7         BigInteger p = BigInteger.ONE;
 8         // 依靠標志位判斷是否結束線程
 9         while(!canceled){
10             p = p.nextProbablePrime();
11             synchronized (this){
12                 primes.add(p);
13             }
14         }
15     }
16     // 取消
17     public void cancel(){canceled = true;}
18     //返回結果
19     public synchronized List<BigInteger> get(){
20         return primes;
21     }
22 }

  上述代碼設置一個volatile “已請求取消”標志,而任務將定期查看該標志。 PrimeGenerator 將持續的枚舉素數,直到標志位被設置為取消結束。PrimeGenerator  每次枚舉素數時候都會檢查canceled標志位是否被改變。

 1 public List<BigInteger> aPrimes() throws InterruptedException {
 2         PrimeGenerator generator = new PrimeGenerator();
 3         new Thread(generator).start();
 4         try{
 5             // 睡眠1秒
 6             TimeUnit.SECONDS.sleep(1);
 7         }finally {
 8             // 1秒后取消
 9             generator.cancel();
10         }
11         return generator.get();
12 }

  調用素數生成器運行1秒后取消,值得注意的是,素數生成器可能不會在1秒后“准時”停止,因為他可能此時剛好在while內執行。取消語句放在finally語句執行,保證該語句一定會被執行。

2、取消策略

  在設計良好的程序中,一個可取消的任務必須擁有取消策略,這個策略詳細定義取消操作的“How”、“When”、“What”,即代碼如何(How)請求取消該任務,任務在何時(When)檢查是否已經請求了取消,以及在響應時執行那些(What)操作。

  在上述代碼中,PrimeGenerator采用了簡單的取消策略:客戶代碼通過canceled來請求取消,PrimeGenerator在每次執行搜索前首先檢查是否存在取消請求,如果存在則退出。

3、中斷線程

  PrimeGenerator 中取消機制之所以能成功,是因為程序會不間斷定期的檢查標志位的狀態是否被改變。但是,如果程序調用了一個阻塞方法,例如,BlockingQueu.put()那么可能會出現問題,即任務可能永遠不會檢查取消標志。【阻塞隊列不了解的看看這篇博客:http://www.cnblogs.com/moongeek/p/7832855.html#_label3

 1 // 不推薦的寫法
 2 public class BrokenPrimeProducer extends Thread {
 3     // 阻塞隊列
 4     private final BlockingQueue<BigInteger> queue;
 5     // 中斷位
 6     private volatile boolean canceled = false;
 7     
 8     public BrokenPrimeProducer(BlockingQueue<BigInteger> queue){
 9         this.queue = queue;
10     }
11     
12     @Override
13     public void run(){
14         try {
15             BigInteger p = BigInteger.ONE;
16             while (!canceled) {
17               // PUT操作可能會被阻塞,將無法檢查 canceled 是否變化,因而無法響應退出
18                 queue.put(p = p.nextProbablePrime());
19             }
20         }catch (InterruptedException ex){}
21     }
22     
23     public void cancel(){
24         canceled = true;
25     }
26 }

  如果阻塞隊列在 put()  操作被阻塞,此時,即使我們調用cancel() 方法將狀態變量改變,進程也無法檢查到改變,因為會一直阻塞下去。

  每個Thread都有一個boolean類型的中斷狀態。當中斷線程時,改狀態會被置為true。Thread中包含的中斷方法如下。其中 inturrept() 會將中斷狀態置為true,而 isInterrupted() 方法會返回當前的中斷狀態,而 interrupted() 方法則會清除當前狀態,並返回它之前的值。

1 public class Thread{
2     public void inturrept(){......}
3       public boolean isInterrupted(){......}
4       public static boolean interrupted(){......}
5 }

  通常情況下,如果一個阻塞方法,如:Object.wait()、Thread.sleep()Thread.join() 時,都會去檢查中斷狀態的值,發現中斷狀態變化時都會提前返回並響應中斷:清除中斷狀態,並拋出InterruptedException異常

  該注意的是,中斷操作並不會真正的中斷一個正在運行的線程,而只是發出中斷請求,然后由程序在合適的時刻中斷自己。一般設計方法時,都需要捕獲到中斷異常后對中斷請求進行某些操作,不能完全忽視或是屏蔽中斷請求。

  對上代碼進行改進,采用中斷進行中斷程序執行。代碼中有兩處可以檢測中斷:在阻塞的put() 方法中,以及循環開始處的查詢中斷狀態時。其實put() 操作會檢測響應異常,在循環開始時可以不進行檢測,但這樣可以獲得更高效的響應性能。

 1 public class PrimeProducer extends Thread {
 2     // 阻塞隊列
 3     private final BlockingQueue<BigInteger> queue;
 4     
 5     public PrimeProducer(BlockingQueue<BigInteger> queue){
 6         this.queue = queue;
 7     }
 8 
 9     @Override
10     public void run(){
11         try {
12             BigInteger p = BigInteger.ONE;
13             while (!Thread.currentThread().isInterrupted()) {
14                 queue.put(p = p.nextProbablePrime());
15             }
16         }catch (InterruptedException ex){
17             // 允許退出線程
18         }
19     }
20 
21     public void cancel(){
22         // 中斷
23         interrupt();
24     }
25 }

  中斷是實現取消的最合理方式,在取消之外的其他操作中使用中斷,都是不合理的。

4、中斷策略

  中斷策略解釋某個中斷請求:當發現中斷請求時,應該做哪些工作,以多快的速度來響應中斷。任務一般不會在其自己擁有的線程中執行,而是在其他某個服務(比如說,在一個其他線程或是線程池)中執行。對於非線程所有者而言(例如,對線程池來說,任何線程池實現之外的代碼),應該保存並傳遞中斷狀態,使得真正擁有線程的代碼才能對中斷做出響應。

  比如說,如果你書寫一個庫函數,一般會拋出InterruptedException作為中斷響應,而不會在庫函數時候把中斷異常捕獲並進行提前處理,而導致調用者被屏蔽中斷。因為你不清楚調用者想要對異常進行何種處理,比如說,是接收中斷后立即停止任務還是進行相關處理並繼續執行任務。中斷的處理必須由該任務自己決定,而不是由其他線程決定。

  因為在捕獲InterruptException 中會同時把中斷位恢復,所以,如果想捕獲異常后恢復中斷位,一般會調用 Thread.currentThread.interrupt() 進行中斷位的恢復。

1  try {
2     // dosomething();
3  } catch (InterruptedException e) {
4     // 捕獲異常后恢復中斷位
5    Thread.currentThread().interrupt();
6    e.printStackTrace();
7  }

5、使用Future 來實現取消

  關於Future 對象:ExecutorService.submit 方法將返回一個Future 來描述任務。

 1 public interface Future<V> {
 2     // 是否取消線程的執行
 3     boolean cancel(boolean mayInterruptIfRunning);
 4     // 線程是否被取消
 5     boolean isCancelled();
 6     //線程是否執行完畢
 7     boolean isDone();
 8       // 立即獲得線程返回的結果
 9     V get() throws InterruptedException, ExecutionException;
10       // 延時時間后再獲得線程返回的結果
11     V get(long timeout, TimeUnit unit)
12         throws InterruptedException, ExecutionException, TimeoutException;
13 }
 1 public static void main(String[] args) {
 2         ExecutorService service = Executors.newSingleThreadExecutor();
 3         Future future = service.submit(new TheradDemo());
 4 
 5         try {
 6           // 可能拋出異常
 7             future.get();
 8         } catch (InterruptedException e) {
 9             e.printStackTrace();
10         } catch (ExecutionException e) {
11             e.printStackTrace();
12         }finally {
13           //終止任務的執行
14             future.cancel(true);
15         }
16  }

  Future 中的  cancel(boolean mayInterruptIfRunning) 接受一個布爾參數表示取消操作是否成功。如果Future.get()  拋出異常,如果你不需要得到結果時,就可以通過cancel(boolean) 來取消任務。

  對於線程池中的任務,如果想想要取消執行某任務,不宜中斷線程池,因為你不知道中斷請求到達時正在執行什么任務,所以只能通過cancel(boolean) 來定向取消特定的任務。

6、關閉ExecutorService

  線程池相關對象ExecutorService 提供了兩種關閉的方法:使用 shutdown() 正常關閉,他先把線程池狀態設置為SHUTDOWN ,禁止再向線程池提交任務,然后把線程池中的任務全部執行完畢,就關閉線程池。這種方法速度較慢,但是更安全。以及使用shutdownNow() 首先關閉正在執行的任務,然后返回所有尚未啟動的任務清單。這種方法速度快,但風險也大,因為有的任務可能執行了一般被關閉。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM