Java并发学习之中断策略

一. 中断策略学习

我们前面学习了任务包含取消策略,同样的,线程应该包含中断策略。

1.中断策略规定线程如何解释某个中断请求:当发现中断请求时,线程应该做哪些工作(如果需要的话),哪些工作单元对于中断来说是原子操作,以及以多快的速度来响应中断。

2.最合理的中断策略是某种形式的线程级取消操作或者服务级取消操作:尽快退出,在必要时清理,通知某个所有者该线程已经退出。

3.除此之外还可以建立其他的中断策略:例如暂停服务或者重新开始服务,但对于哪些包含非标准中断策略的线程或线程池,只能用于能知道这些策略的任务中。

4.区分任务和线程对中断的反应是很重要的。一个中断请求可以有一个或多个接收者:中断线程池中的某个工作者线程,同时意味着“ 取消当前任务 ” 和 “ 关闭工作者线程 ”。

5.任务不会在其自己拥有的线程中执行,而是在某个服务(例如线程池)拥有的线程中执行。

6.对于非线程所有者的代码来说(例如,对于线程池而言,任何在线程池实现之外的代码就是非线程所有者代码)应该小心地保存中断状态,这样拥有线程的代码才能对中断做出响应(即使 非线程所有者 代码也可以对中断做出响应)。
这就是为什么大多数可阻塞的库函数都只是抛出InterruptedException 作为中断响应。它们永远都不会在某个由自己拥有的线程中运行,因此它们为任务或者库代码实现了最合理的取消策略:尽快退出执行流程,并把中断信息传递给调用者,从而使调用栈中的上层代码可以采取进一步的操作。

7.当检查到中断请求时,任务并不需要放弃所有的操作:它可以推迟处理中断请求,并直到某个更合适的时刻。因此需要记住中断请求,并在完成当前任务后抛出InterruptedException或者表示已收到中断请求。

8.任务不应该对执行该任务的线程的中断策略做出任何假设,除非该任务被专门设计为在服务中运行,并且在这些服务中包含特定的中断策略。

9.无论任务把中断视为取消,还是其他某个中断响应操作,都应该小心地保存执行线程的中断状态。
如果除了将InterruptedException传递给调用者外还需要执行其他操作,那么应该在捕获InterruptedException之后恢复中断状态:Thread.currentThread().interrupt();

10.正如任务代码不应该对其执行所在的线程的中断策略做出假设,执行取消操作的代码也不应该对线程的中断策略做出假设。

11.线程应该只能由其所有者中断,所有者可以将线程的中断策略信息封装在某个合适的取消机制中,例如shutdown方法。

注意:由于每个线程拥有各自的中断策略,因此除非你知道中断对该线程的含义,否则就不应该中断这个线程。

二.响应中断

1.当调用可中断的阻塞函数时,例如:Thread.sleep 或者 BlockingQueue.put等,有两种实用策略可用于处理InterruptedException:
1)传递异常(可能在执行某个特定任务的清除操作后),从而使我们的方法也成为可中断的阻塞方法。
2)恢复中断状态,从而使调用栈中的上层代码能够对其进行处理。

传递InterruptedException就是将它抛给调用者,让调用者处理

BlockingQueue<Task> queue;
...
pubic Task getNextTask() throws InterruptedException{
  return queue.take();
}

如果不想或者无法传递InterruptedException,那么需要寻找另一种方式来保存中断请求:
a.一种标准的方式就是通过再次调用interrupt来恢复中断状态。
b.我们不能屏蔽InterruptedException(例如在catch块中捕获到异常却不做任何处理)除非我们的代码中实现了线程中断策略。由于大多数代码并不知道它们将在哪个线程中运行,因此应该保存中断状态。

注意:只有实现了线程中断策略的代码才可以屏蔽中断请求,在常规的任务和库代码中都不应该屏蔽中断请求。

2.对于一些不支持取消但仍可以调用可中断阻塞方法的操作,它们必须在循环中调用这些可中断阻塞方法,并在发现中断后重新尝试,在这种情况下,它们应该在本地保存中断状态,并在返回前恢复中断状态而不是在捕获InterruptedException时恢复中断状态。

如下:

public Task getNextTask(BlockingQueue<Task> queue>){
   boolean interrupted = false;
   try{
     while(true){
       try{
           return queue.take();
       }catch(InterruptedException e){
          interrupted = true;
          return queue.take()//重新尝试
       }
     }
   }finally{
      if(interrupted)
         Thread.currentThread().interrupt();
   }
}

上面代码如果过早地设置中断状态,就可能引起无限循环,因为大多数可中断的阻塞方法都会在入口处检查中断状态,并且当发现该状态已被设置时会立即抛出InterruptedException 。通常可中断的方法会在阻塞或进行重要的工作前首先检查中断,从而尽快的响应中断。

3.如果代码不会调用可中断的阻塞方法,那么仍然可以通过在任务代码中轮询当前线程的中断状态来响应中断。 要选择合适的轮询频率,就需要在效率和响应性之间进行权衡。如果响应性要求较高,那么不应该调用那些执行时间较长并且不响应中断的方法,从而对可调用的库代码进行一些限制。

在取消的过程中可能涉及除了中断状态之外的其他状态。中断可以用来获得线程的注意,并且由中断线程保存的信息,可以为中断的线程提供进一步的提示。(当访问这些信息的时候,要确保使用同步)

例如,当一个由ThreadPoolExecutor拥有的工作者线程检测到中断状态时,它会检查线程池是否正在关闭,如果是,它会结束之前执行线程池清理工作,否则它可能会创建一个新线程将线程池恢复到合理的规模。

4.实例学习:计时运行
在实际的生活中,许多问题永远也无法解决(比如枚举所有的素数),而某些问题能很快得到的答案。在这种情况下,如果能够指定 “最多花10分钟搜索答案” 或者 “枚举出在10分钟内能找到的答案” 会非常有用。
在实际的编程中也是如此,如果我们可以控制某个任务执行的时间将会极大的提高我们的程序的效率。
那么如何达到这个效果呢? 我们先看一下下面的代码:

List<BigInteger> aSecondOfPrimes() throws InterruptedException{
  PrimeGenerator generator = new PrimeGenerator();//素数生成器任务
  new Thread(generator).start();//启动任务
  try{
    SECONDS.sleep(1);
  }finally{
    generator.cancel();
  }
  return generator.get();
}

上面的代码中aSecondOfPrimes方法将启动一个PrimeGenerator任务,并在1秒钟后中断。
很明显由于执行的顺序,PrimeGenerator可能需要超过1秒的时间才能停止,但它最终会发现中断,然后停止,并使线程结束。
在执行任务时的另一个方面是,我们希望知道在任务执行过程中是否会抛出异常。
如果PrimeGenerator在指定的时限内抛出一个未检查的异常,那么这个异常可能被忽略,因为素数生成器在另一个独立的线程中运行,而这个线程并不会显式地处理异常。

下面的代码解决了从任务中抛出未检查的异常问题(但不建议这么做):

private static final ScheduledExecutorService cancelExec = 
                                       Executors.newScheduledThreadPool(10);

public static void timedRun(Runnable r, long timeout, TimeUnit unit){
   final Thread taskThread = Thread.currentThread();//获得当前线程
   //设置线程在指定时间后中断
   cancelExec.schedule(new Runnable(){
       public void run(){
        taskThread.interrupt();
       }
  },timeout,uint);
  
  r.run();//任务执行
}

上面代码中给出了在指定的时间内运行一个任意的Runnable的示例,它在调用线程中运行任务,并安排了一个取消任务,在运行指定的时间间隔后中断它,这解决了从任务中抛出未检查异常的问题,因为该异常会被timedRun的调用者捕获。

注意:上面的做法非常简单,但却破坏了下面规则:在线程中断之前,应该了解它的中断策略。

由于timedRun可以从任意一个线程中调用,因此它无法知道这个调用线程的中断策略。
如果任务在超时之前完成,那么中断timedRun所在的线程的取消任务将在timedRun返回到调用者之后启动。我们不知道在这种情况下将运行什么代码,但结果一定是不好的。
而且,如果任务不响应中断,那么timedRun会在任务结束时才返回,此时可能已经超过了指定的时限,如果某个限时运行的服务没有在指定的时间内返回,那么将会对调用者带来负面影响。

那么有没有更好的方法呢? 当然有!!
请看下面的代码:
即使任务不响应中断,timedRun仍能按时返回。

public static void timedRun(final Runnable r,long timeout,TimeUnit uint)
                                                    throws InterruptedException{
    class RethrowableTask implements Runnable{
        private volatile Throwable t;
        public void run(){
          try{ 
             r.run(); 
          }catch(Throwable t){
             this.t=t;
          }
        }
        
        void rethrow(){
          if(t != null)
             throw launderThrowable(t);
        }
    }
    
    RethrowableTask task = new RethrowableTask();
    final Thread taskThread = new Thread(task);
    taskThread.start();//开始线程
    cancelExec.schedule(new Runnable(){
         public void run(){
           taskThread.interrupt();
         }
    },timeout,uint);
    
    taskThread.join(uint.toMillis(timeout));//使线程等待指定的时间后死亡
    task.rethrow();
}
                                                    

上面的代码虽然解决了前面示例中的问题,但由于它依赖于一个限时的join,因此存在着join的不足:无法知道执行控制是因为线程正常退出而返回还是因为join超时而返回。

那么又没有更好的选择来实现呢?

我们可以通过Future来实现取消:

public static void timedRun(Runnable r, long timeout, TimeUnit unit) 
throws InterruptedException{
  Future<?> task = taskExec.submit(r);
  
  try{
   task.get(timeout , unit);
  }catch(TimeoutException e){
    //接下来任务将被取消
    task.cancel(true);
  }catch(ExecutionException e){
     //如果在任务中抛出了异常,那么重新抛出该异常
     throw launderThrowable(e.getCause());
  }finally{
      //如果任务已经结束,那么执行取消操作也不会带来任何影响
      task.cancel(true);//如果任务正在运行,那么将被中断
  }
}

注意:当Future.get抛出InterruptedException或者TimeoutException时,如果你知道不再需要结果,那么就可以调用Future.cancel()来取消任务。

总结:了解中断策略有助于我们对框架的使用,会然我们编出更好的并发程序。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序员小牧之

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值