java並發基礎(四)--- 取消與關閉
《java並發編程實戰》的第7章是任務的取消與關閉。我覺得這一章和第6章任務執行同樣重要,一個在行為良好的軟件和勉強運行的軟件之間的最主要的區別就是,行為良好的軟件能很完善的處理失敗、關閉和取消等過程。
一、任務取消
在java中沒有一種安全的搶占式(收到中斷請求就立刻停止)的方式來停止線程,因此也沒有安全的搶占式方法來停止任務。只有一些協作式的機制,比如設置請求已取消的標識。在下面的例子中,PrimeGenarator持續的列出素數,直到它被取消。cancel方法將設置cancelled標識,並且主循環在搜索下一個素數之前會檢查這個標識,為了使這個過程可靠工作,cancelled必須是volatile類型。
//使用volatile類型的域來保護取消狀態 public class PrimeGenerator implements Runnable{ private final List<BigInteger> primes = new ArrayList<BigInteger>(); private volatile boolean cancelled; @Override public void run() { BigInteger p = BigInteger.ONE; while (!cancelled) { p= p.nextProbablePrime(); synchronized (this) { primes.add(p); } } } public void cancel(){cancelled = true;} public synchronized List<BigInteger> get(){ return new ArrayList<BigInteger>(primes); } }
測試,讓genarator只執行一秒。
public static void main(String[] args) throws Exception{ PrimeGenerator generator = new PrimeGenerator(); new Thread(generator).start(); try { Thread.sleep(1000); }finally{ generator.cancel(); } System.out.println(generator.get());; }
打印結果就不陳列了,無非就是一些素數而已。
二、中斷
每一個線程都有一個boolean類型的中斷狀態。當中斷線程時,這個線程的中斷狀態將被設置為true ,在Thread中包含了中斷線程以及查詢線程中斷狀態的方法,如下:
public class Thread{ //中斷目標線程(但線程不會立刻停止,也就是不會搶占式停止) public void interrupt(){} //返回線程的中斷狀態 已中斷:true 未中斷:false public boolean isInterrupted(){} //清除當前線程的中斷狀態,並返回它之前的值,這是清除中斷狀態的唯一方法 public static boolean interrupted(){} }
如果一個線程被中斷,會發生兩件事情:1.清除中斷狀態 2.拋出InterruptedException異常,所以,有時候如果捕獲了InterruptedException後還要有其他操作的話,要把當前線程中斷:Thread.currentThread().interrupt();然後再做其他事。
還有一點需要註意,調用interrupt並不意味著立即停止目標線程正在進行的工作,而只是傳遞了請求中斷的消息。對中斷操作的正確理解是:它並不會真正的中斷一個正在運行的線程,而只是發出中斷請求,然後由線程在下一個合適的時刻自己中斷。
現在我們回過頭來看PrimeGenarator的例子,如果代碼中出現了阻塞隊列,那這種用volatile做標識的取消就可能會有問題,比如基於生產者和消費者模式的素數生成:
//生產者 class BrokenPrimeProducer extends Thread{ private final BlockingQueue<BigInteger> queue; private volatile boolean cancelled = false; public BrokenPrimeProducer(BlockingQueue<BigInteger> queue) { this.queue = queue; } @Override public void run() { try { BigInteger p = BigInteger.ONE; while (!cancelled) { queue.put(p = p.nextProbablePrime()); } } catch (InterruptedException e) { } } public void cancel(){cancelled = true;} }
消費者的代碼就不寫了,無非是從阻塞隊列中取出素數。你看,這個時候,生產者線程生成素數,並將它們放入阻塞隊列中,如果生產者的速度超過了消費者的處理速度,隊列將被填滿,put方法也會被阻塞,如果這時消費者想取消生產者這個任務就無濟於事了,因為生產者阻塞在了put方法中。這個問題很好解決,使用中斷來替代boolean標識。修改生產者代碼:
class PrimeProducer extends Thread{ private final BlockingQueue<BigInteger> queue; PrimeProducer(BlockingQueue<BigInteger> queue){ this.queue = queue; } @Override public void run() { try { BigInteger p = BigInteger.ONE; //條件改為當前線程是否中斷 while (!Thread.currentThread().isInterrupted()) { queue.put(p = p.nextProbablePrime()); } } catch (InterruptedException e) { /*允許線程退出*/ } } public void cancel(){interrupt();} }
消費者如果不再需要生產者,可以直接中斷生產者線程,這樣即使生產者處於阻塞狀態,一樣可以退出。由此可見,中斷是實現取消的最合理方式。
java並發基礎(四)--- 取消與關閉