Java并发编程实战 - 取消与关闭

estren83 8年前
   <p>写在前面</p>    <p>任务和线程的启动是一件非常容易的事情。在大多时候,我们都会让它们从开始运行到结束,或者让它们自行停止。然而,有的时候我们希望提前结束任务或者是线程:有可能是它们运行时发生了错误;有可能是用户取消了操作,或者是应用程序需要被快速关闭。可是要是任务和线程快速、安全地停下来,并不是一件十分容易的事情。Java中也没有提供任何安全的机制能够使它们停下来(虽然Thread.stop和suspend等方法提供了这样的机制,但是它们存在一些严重的缺陷,应该避免使用)。但Java提供了 <strong> 中断(Interruption) </strong> ,这是一种协作机制,能够使一个线程阻止另一个线程现在正在进行的工作。这种良好的协作机制是必须的,我们不希望某任务、线程或者是服务立即停止,因为这种立即停止会使得共享的数据结构处于不一致的状态。相反,在编写任务和服务的时候我们可以使用一种协作机制: <strong>当需要停止的时候,它们会首先清楚当前正在执行的工作,然后再结束。这提供了更好的灵活性,因为任务本身的代码比发出取消请求的代码更清楚该如何清楚该如何执行清楚工作。</strong></p>    <p>生命周期结束(End-of-lifecycle)的问题会使任务、服务以及程序的设计以及实现的过程变得复杂,并且这个在程序设计的过程当中尝尝被忽略,但它又是非常重要的。一个运行良好的软件和一个运行情况很糟糕的软件的区别就在于:行为良好的软件能很完善地处理失败、关闭和取消等过程。</p>    <p>下面将给出各种实现取消和中断的机制,以及如何编写任务和服务,是它们能够对取消请求做出响应。</p>    <h3><strong>任务取消</strong></h3>    <p>如果外部代码能够在某个操作正常完成之前将其置入“完成”状态,那么这个操作就可以成为可取消的(Cancellable)。取消某个操作的原因有很多:</p>    <ul>     <li>用户请求取消:<br> 用户点击“取消”按钮或者通过管理接口来发送取消请求;</li>     <li>有时间限制的操作:<br> 例如某个应用程序需要在某个特定的时间完成并返回,如果到了规定的时间没有完成,那么当计时器超时时则取消正在进行的任务;</li>     <li>应用程序事件:<br> 例如,应用程序对某个问题空间进行分解并搜索,从而使不同的任务可以搜索问题空间中的不同区域。当其中一个任务找到了解决方案时,所有其他正在进行的搜索任务都将被取消。</li>     <li>错误:<br> 当程序在运行当中发生了错误时,程序应该保存当前的状态然后取消接下来的任务。</li>     <li>关闭:<br> 当程序或者服务关闭时,必须对正在运行或处理的工作执行某种操作。在平缓的关闭过程中,当前工作将继续执行直到完成,而在立即关闭的过程中,当前任务(工作)可能被取消。</li>    </ul>    <p><strong>能设置某个“已请求取消(Cancellation Requested)”标志的协作机制:</strong></p>    <p>任务将定期查看该标志。如果设置了该标志,那么任务将提前结束。</p>    <p>下面的PrimeGenerator使用了简单的取消策略:客户端通过调用cancel来请求取消,PrimeGenerator在每次搜索素数之前都先检查一下是否存在取消请求,如果存在则退出。</p>    <pre>  <code class="language-java">//其中的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();  }</code></pre>    <p><strong>可靠的取消策略</strong></p>    <p>一个可靠的取消策略应该有自己的“HWW”原则:</p>    <ul>     <li> <p>How:外部代码如何请求取消该任务?</p> </li>     <li> <p>When:外部代码何时取消该任务?</p> </li>     <li> <p>What:在响应取消操作时应该进行哪些操作?</p> </li>    </ul>    <p><strong>通常,中断是实现取消的最合理的方式。</strong></p>    <p>上面的PrimeGenerator中的取消机制最终会使搜索素数的任务退出,但在退出过程中需要花费一定的时间。然而,如果使用这种方法的任务调用了一个阻塞方法,例如BlockingQueue.put,那么可能会产生一个更为严重的问题:任务可能永远不会检查取消标志,因此永远不会结束。</p>    <p>如下面程序所示:</p>    <p>生产者生产素数,并将它们放入一个阻塞队列。如果生产者的速度超过了消费者的处理速度,队列将被填满,put方法就会被阻塞。当生产者在put方法中阻塞时,如果消费者希望取消生产者任务,那么将发生什么情况呢?它可以调用cancel方法来设置cancelled标志,但此时生产者却永远不能检查这个标志,因为它无法从阻塞的put方法中恢复过来(因为消费者此时已经停止从队列中取出素数,所以put方法将一直保持阻塞状态)。</p>    <pre>  <code class="language-java">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();                  }              }      }</code></pre>    <p><em>那么如果程序能够响应中断,就可以使用中断作为取消机制了,不是吗?</em></p>    <p><em>那再看看下面这个程序:</em></p>    <p>这里是通过使用中断而不是boolean标志来请求取消。在每次迭代循环当中,有两个位置可以检测出中断:在阻塞的put方法调用中,以及在循环开始处查询中断状态时。由于调用了阻塞的put方法,因此这里不一定需要显示的检测,但执行检测却会使PrimeProducer对中断具有更高的响应性,因为它是在启动寻找素数任务之前检查中断的,而不是在任务完成之后。如果可中断的阻塞方法的调用频率不高,不足以获得足够的响应性,那么显式地检测中断状态能起到一定的帮助作用。</p>    <pre>  <code class="language-java">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();          }  }</code></pre>    <p> </p>    <p>来自:http://www.jianshu.com/p/9b8a7ec9f616</p>    <p> </p>