并发编程实战:取消和关闭

任务和线程的启动很容易。在大多数时候,我们都会让它们运行直到结束,或者让它们自行停止。然而,有时候我们希望提前结束任务或线程,或许是因为用户取消了操作,或者应用程序需要被快速关闭。

要使任务和线程能安全、快速、可靠地停止下来,并不是一件容易的事。Java没有提供任何机制来安全地终止线程(虽然Thread.stop和suspend等方法提供了这样的机制,但是由于存在着一些严重的缺陷,因此避免使用),但它提供了中断(Interruption),这是一种协作机制,能够使一个线程终止另一个线程的当前工作。

一、任务取消

如果外部代码能在某个操作正常完成之前将其置入“完成”状态,那么这个操作就可以称为可取消的(Cancellable)。取消某个操作的原因很多:

1.用户请求取消
2.有时间限制的操作
3.应用程序事件
4.错误:
5.关闭:
在java中没有一种安全的抢占式方法来停止线程,因此也就没有安全的抢占式方法来停止任务。只有一些协作式的机制,使请求取消的任务和代码都遵循一种协商好的协议。

其中一种协作机制能设置某个“已请求取消”标志,而任务将定期地查看该标志。如果设置来这个标志,那么任务将提前结束。

复制代码

示例

其中PrimeGenerator持续地枚举素数,直到它被取消。cancel方法将设置cancelled标志,并且主循环在搜索下一个素数之前会首先检查这个标志(为了使这个过程可靠的工作,标志cancelled必须为volatile类型)

pulbic  class PrimeGenerator implements Runnable{
    private final List<BigInteger> primes = new ArrayList<BigInteger>;
    //使用volatile类型的域来保存取消状态
    private volatile boolean cancelled;
    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(){
        teturn new ArrayList<BigInteger>(primes);
    }
    
}

复制代码

下边是PrimeGenerator的使用示例,即让素数生成器运行1秒钟后取消。素数生成器通常不会刚好在运行一秒钟后停止,因为在请求取消的时刻和run方法中循环执行下一次检查之间可能存在延迟。cancel方法由finally块调用,从而确保即使在调用sleep时被中断也能取消素数生成器的执行。如果cancel没有被调用,那么搜索素数的线程将永远运行下去,不断消耗CPU的时钟周期,并使得JVM不能正常退出。

List<BigInteger> aSecondOfPeimes() throws InterruptionException{
           PrimeGenerator primeGenerator = new PrimeGenerator();
           new Thread(primeGenerator).start();
           try{
              SECONDS.sleep(1);  
           }finally {
               primeGenerator.cancel();
           }
           return primeGenerator.get();
	}
primeGenerator使用了一种简单的取消策略:客户代码通过调用cancel来请求取消,primeGenerator在每次搜索素数之前首先检查是否存在取消请求,如果存在则退出。
复制代码

总结:

一个可取消的任务必须拥有取消策略(Cancellation Policy),在这个策略中将详细地定义取消操作的“How”、“When”、“What”,即其他代码如何(How)请求取消任务,任务在何时(When)检查是否已经请求了取消,以及在响应取消请求时应该执行哪些(What)操作。

举例

考虑现实世界中停止支付支票的示例。银行通常会规定如何提交一个停止支付的请求,在处理这些请求时需要作出哪些响应性保证,以及当支付中断后需要遵守哪些流程(例如通知该事务中涉及的其他银行,以及对付款人的账户进行费用评估)。这些流程和保证放在一起就构成了支票支付的取消策略。

二、中断

primeGenerator中的取消机制最终会使得搜索素数的任务退出,但在退出过程中需要花费一定的时间,然而,如果使用这种方法的任务调用一个阻塞方法,例如BlockingQueue.put,那么可能会产生一个更严重的问题——任务可能永远不会检查取消标志,因此永远不会结束。

在程序就说明了这个问题。生产者线程生成素数,并将它们放入一个阻塞队列。如果生产者的速度超过了消费者的处理速度,队列将被填满,put方法也会阻塞。当生产者在put方法中阻塞时,如果消费者希望取消生产者任务,那么将会发生什么情况呢??它可以调用cancel方法来设置cancalled标志,但此时生产者却永远不能检测这个标志,因为它无法从阻塞的put方法中恢复过来(因为消费者此时已经停止从队列中取出素数,所以put方法将一直保持阻塞状态)。

猜你喜欢

转载自juejin.im/post/5cc93ba36fb9a031ef63c5bb