Java并發編程實戰 - 取消與關閉

estren83 8年前發布 | 8K 次閱讀 Java 并發 Java開發

寫在前面

任務和線程的啟動是一件非常容易的事情。在大多時候,我們都會讓它們從開始運行到結束,或者讓它們自行停止。然而,有的時候我們希望提前結束任務或者是線程:有可能是它們運行時發生了錯誤;有可能是用戶取消了操作,或者是應用程序需要被快速關閉。可是要是任務和線程快速、安全地停下來,并不是一件十分容易的事情。Java中也沒有提供任何安全的機制能夠使它們停下來(雖然Thread.stop和suspend等方法提供了這樣的機制,但是它們存在一些嚴重的缺陷,應該避免使用)。但Java提供了 中斷(Interruption) ,這是一種協作機制,能夠使一個線程阻止另一個線程現在正在進行的工作。這種良好的協作機制是必須的,我們不希望某任務、線程或者是服務立即停止,因為這種立即停止會使得共享的數據結構處于不一致的狀態。相反,在編寫任務和服務的時候我們可以使用一種協作機制: 當需要停止的時候,它們會首先清楚當前正在執行的工作,然后再結束。這提供了更好的靈活性,因為任務本身的代碼比發出取消請求的代碼更清楚該如何清楚該如何執行清楚工作。

生命周期結束(End-of-lifecycle)的問題會使任務、服務以及程序的設計以及實現的過程變得復雜,并且這個在程序設計的過程當中嘗嘗被忽略,但它又是非常重要的。一個運行良好的軟件和一個運行情況很糟糕的軟件的區別就在于:行為良好的軟件能很完善地處理失敗、關閉和取消等過程。

下面將給出各種實現取消和中斷的機制,以及如何編寫任務和服務,是它們能夠對取消請求做出響應。

任務取消

如果外部代碼能夠在某個操作正常完成之前將其置入“完成”狀態,那么這個操作就可以成為可取消的(Cancellable)。取消某個操作的原因有很多:

  • 用戶請求取消:
    用戶點擊“取消”按鈕或者通過管理接口來發送取消請求;
  • 有時間限制的操作:
    例如某個應用程序需要在某個特定的時間完成并返回,如果到了規定的時間沒有完成,那么當計時器超時時則取消正在進行的任務;
  • 應用程序事件:
    例如,應用程序對某個問題空間進行分解并搜索,從而使不同的任務可以搜索問題空間中的不同區域。當其中一個任務找到了解決方案時,所有其他正在進行的搜索任務都將被取消。
  • 錯誤:
    當程序在運行當中發生了錯誤時,程序應該保存當前的狀態然后取消接下來的任務。
  • 關閉:
    當程序或者服務關閉時,必須對正在運行或處理的工作執行某種操作。在平緩的關閉過程中,當前工作將繼續執行直到完成,而在立即關閉的過程中,當前任務(工作)可能被取消。

能設置某個“已請求取消(Cancellation Requested)”標志的協作機制:

任務將定期查看該標志。如果設置了該標志,那么任務將提前結束。

下面的PrimeGenerator使用了簡單的取消策略:客戶端通過調用cancel來請求取消,PrimeGenerator在每次搜索素數之前都先檢查一下是否存在取消請求,如果存在則退出。

//其中的PrimeGenerator持續地枚舉素數,直到它被取消。calcel方法將設置cancelled標志,并且主循環
//在搜索下一個素數之前會首先檢查這個標志。(為了讓這個過程可靠地工作,標志cancelled必須為volatile)
public class PrimeGenerator implements Runnable{
        private final List<BigInteger> primes = new ArrayList<BigInteger>();
        //使用volatile類型的域來保存取消狀態
        private volatile boolean cancelled;
        public void run(){
            //從1開始
            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);
        }
}
//讓素數生成器運行一秒鐘之后取消。素數生成器可能并不會在剛好運行了一秒鐘的時候取消,因為在請求
//取消時刻和run方法中循環執行下一次檢查之間可能存在延遲。cancle方法由finally調用,即使是在調用
//sleep時被中斷也能取消素數生成器的運行。如果素數生成器沒有被取消,那么它將一直運行下去,
//不斷消耗CPU時鐘周期,使得JVM不能正常退出。
List<BigInteger> aSecondPrimes() throws InterruptedException{
    PrimeGenerator generator = new PrimeGenerator();
    new Thread(generator).start();
    try{
        SECONDS.sleep(1);
    }finally{
        generator.cancel();
    }
    return generator.get();
}

可靠的取消策略

一個可靠的取消策略應該有自己的“HWW”原則:

  • How:外部代碼如何請求取消該任務?

  • When:外部代碼何時取消該任務?

  • What:在響應取消操作時應該進行哪些操作?

通常,中斷是實現取消的最合理的方式。

上面的PrimeGenerator中的取消機制最終會使搜索素數的任務退出,但在退出過程中需要花費一定的時間。然而,如果使用這種方法的任務調用了一個阻塞方法,例如BlockingQueue.put,那么可能會產生一個更為嚴重的問題:任務可能永遠不會檢查取消標志,因此永遠不會結束。

如下面程序所示:

生產者生產素數,并將它們放入一個阻塞隊列。如果生產者的速度超過了消費者的處理速度,隊列將被填滿,put方法就會被阻塞。當生產者在put方法中阻塞時,如果消費者希望取消生產者任務,那么將發生什么情況呢?它可以調用cancel方法來設置cancelled標志,但此時生產者卻永遠不能檢查這個標志,因為它無法從阻塞的put方法中恢復過來(因為消費者此時已經停止從隊列中取出素數,所以put方法將一直保持阻塞狀態)。

class BrokenPrimeGenerator extends Thread{
        private final BlockingQueue<BigInteger> queue;
        private volatile boolean cancelled;
        BrokenPrimeGenerator(BlockingQueue<BigInteger> queue){
            this.queue = queue;
        }
        public void run(){
            BigInteger p = BigInteger.ONE;
            while(!cancelled){
                queue.put(p = p.nextProbablePrime());
                }catch(InterruptedException consumed){}
            }
        }
        public void cancel(){
            cancelled = true;
        }

        void consumePrimes() throws InterruptedException{
            BlockingQueue<BigInteger> primes = new BlockingQueue<BigInteger>();
            BrokenPrimeProducer producer = new BrokenPrimeProducer(primes);
            producer.start();
            try{
                while(needMorePrimes){
                    consume(primes.take())
                }finally{
                    producer.cancel();
                }
            }
    }

那么如果程序能夠響應中斷,就可以使用中斷作為取消機制了,不是嗎?

那再看看下面這個程序:

這里是通過使用中斷而不是boolean標志來請求取消。在每次迭代循環當中,有兩個位置可以檢測出中斷:在阻塞的put方法調用中,以及在循環開始處查詢中斷狀態時。由于調用了阻塞的put方法,因此這里不一定需要顯示的檢測,但執行檢測卻會使PrimeProducer對中斷具有更高的響應性,因為它是在啟動尋找素數任務之前檢查中斷的,而不是在任務完成之后。如果可中斷的阻塞方法的調用頻率不高,不足以獲得足夠的響應性,那么顯式地檢測中斷狀態能起到一定的幫助作用。

class PrimeProducer extends Thread{
        private final BlockingQueue<BigInteger> queue;
        private volatile boolean cancelled;

        PrimeProducer(BlockingQueue<BigInteger> queue){
            this.queue = queue;
        }

        public void run(){
            BigInteger p = BigInteger.ONE;
            while(!Thread.currentThread().isInterrupted(){
                queue.put(p = p.nextProbablePrime());
                }catch(InterruptedException consumed){
                    /*允許線程退出*/
                }
            }

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

 

來自:http://www.jianshu.com/p/9b8a7ec9f616

 

 本文由用戶 estren83 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!