InterruptedException 和 interrupting threads 的一些说明

如果InterruptedException没有检测到异常,可能没人会注意到它,这会导致很多bug不被发现。而检测到这个异常的人大多数都是草率地、不恰当地处理着它。

让我们举一个简单的例子,有一个线程周期性地进行清理工作,其他时间都处于休眠状态:

class Cleaner implements Runnable {

  Cleaner() {
    final Thread cleanerThread = new Thread(this, "Cleaner");
    cleanerThread.start();
  }

  @Override
  public void run() {
    while(true) {
      cleanUp();
      try {
        TimeUnit.SECONDS.sleep(1);
      } catch (InterruptedException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
      }
    }
  }

  private void cleanUp() {
    //...
  }

}

这段代码在很多层面都有错误!
1.在一些环境中,利用构造函数启动Thread可能并不是一个好主意,例如像Spring这类的框架会创造动态子类去支持方法拦截,最终我们会得到从两个不同实例运行产生的两个线程。
2.InterruptedException被吞掉了(吞掉指的是捕捉到了异常,之后继续程序执行,就像没发生一样),异常本身没有被日志正确地记录下来。
3.这个类为每一个实例开启一个新线程,应该使用ScheduledThreadPoolExecutor来代替,由那些实例共同分享线程池(有更高的稳定性和内存效率)。
4.使用ScheduledThreadPoolExecutor我们可以避免手动地编写休眠/工作循环,而且可以切换到fixed-rate(任务执行的间隔周期以一定比率增长)而不是这里的fixed-delay(任务执行间隔周期不变)。
5.最后很重要的是当Cleaner的实例不再被引用时,没有方法销毁它创造的线程。
所有问题都是有效的,但吞掉InterruptedException是最大的问题。在我们搞清楚为什么之前,让我们思考一下这个异常意味着什么,我们怎样才能利用它优雅地打断线程。在JDK中有些阻塞操作声明会抛出InterruptedException:

  • Object.wait()
  • Thread.sleep()
  • Process.waitFor()
  • Process.waitFor()
  • AsynchronousChannelGroup.awaitTermination()
  • java.util.concurrent.*中各种阻塞的方法, 例如 ExecutorService.awaitTermination(), Future.get(), BlockingQueue.take(), Semaphore.acquire(), Condition.await() 还有很多其他的
  • SwingUtilities.invokeAndWait()

注意到阻塞的I/O操作不会抛出InterruptedException(这是个耻辱)。如果所有的类都声明了InterruptedException,你可能想知道这个异常是什么时候被抛出过?

  • 当一个线程被一些声明了InterruptedException的方法阻塞时,你对这个线程调用Thread.interrupt(),那么大多数这样的方法会立即抛出InterruptedException.
  • 如果你向一个线程池提交任务(ExecutorService.submit()),这个任务正在被执行的时候你调用了Future.cancel(ture)
    在这种情况下,线程池会试着为你打断正在执行这个任务的线程,以便有效地打断你的任务。

了解InterruptedException的真正含义,我们就具备了正确处理它的能力。如果有些人试着去打断我们的线程,而我们通过捕捉InterruptedException发现了它,这时候最合理的事情就是结束上述被打断的线程。

class Cleaner implements Runnable, AutoCloseable {

  private final Thread cleanerThread;

  Cleaner() {
    cleanerThread = new Thread(this, "Cleaner");
    cleanerThread.start();
  }

  @Override
  public void run() {
    try {
      while (true) {
        cleanUp();
        TimeUnit.SECONDS.sleep(1);
      }
    } catch (InterruptedException ignored) {
      log.debug("Interrupted, closing");
    }
  }

  //...   

  @Override
  public void close() {
    cleanerThread.interrupt();
  }
}

注意到try-catch块把整个while循环给包起来了,在这种方法中如果sleep()抛出了InterruptedException,我们将退出循环。你可能会说应该将InterruptedException的堆栈跟踪用日志记录下来,这个要视情况而定,在本例中打断一个线程是我们所期望看到的,不是因为失败而产生的。而处理的方法取决于你,底线是如果sleep()被另一个线程打断,我们应该快速地从整个run()中跳出来。如果你很细心的话你可能会问当线程运行到cleanUp()而不是sleep()时被打断,那将会发生什么?你经常会遇到这样的人工标志:

private volatile boolean stop = false;

@Override
public void run() {
  while (!stop) {
    cleanUp();
    TimeUnit.SECONDS.sleep(1);
  }
}

@Override
public void close() {
  stop = true;
}

注意到stop标志(它必须被volatile修饰)不会打断阻塞的操作,我们不得不等待直到sleep()结束。另一方面明确的标志如stop能让我们在任何时刻监控它的值以便更好的控制结束。而且这种方法被证明和线程中断的原理是一样的。如果有些人当线程正执行非阻塞计算的时候(比如cleanUp())试图中断线程,此时这种计算是不能立刻被打断的。然而线程已经被标记为interrupted,在线程的后续操作中(比如sleep())将会立刻直接抛出InterruptedException.
如果我们写了一个非阻塞的线程却仍然想要利用线程中断的便利,我们可以简单周期性地去检查Thread.isInterrupted(),而不必依赖InterruptedException:

public void run() {
  while (!Thread.currentThread().isInterrupted()) {
    someHeavyComputations();
  }
}

对于上面的代码,如果有人想要中断线程,那么一旦someHeavyComputations()返回我们将立刻放弃计算。如果它的执行花费太长时间或者无限制的执行,我们将不会识别到中断标志。有趣的是interrupted标志不是一次性的(一次性指的是改变这个标志的值之后,它就没用了,要想继续使用需要手动把它给变回原样,如那个stop标志),我们能调用Thread.interrupted()而不是isInterrupted(),这样的话interrupted标志就会被重设( Thread.interrupted()会读取并清除中断标志)我们就能够继续我们的工作了。偶尔你想要忽略中断标志,保持程序的运行,在这样的情况下interrupted()就变得非常方便。
注意Thread.stop()
如果你是资历较老的程序员的话,你也许可以调用Thread.stop(),虽然它早已被弃用10年了。Java 8早已计划去”de-implement it”,但在1.8u5中它仍然存在。尽管如此,不要使用它,也不要将Thread.stop()重构到任何代码的Thread.interrupt()中去。
Uninterruptibles from Guava
罕见地,你可能想要完全忽略InterruptedException,对于这种情况你可以查看Guava的Uninterruptibles。它有大量实用的方法像sleepUninterruptibly()和awaitUninterruptibly(CountDownLatch),要小心这些方法。我知道这些方法都没有声明InterruptedException(这个异常可能很棘手),但这些方法却能够完全让当前的线程免于被中断-这可是相当难得的。
总结
到这里我想你已经有些明白为什么某些方法会抛出InterruptedException(你们知道为什么会抛出这个异常吗?是因为某些方法在调用之后会一直阻塞线程,包括大量耗时的计算、让线程等待或者睡眠,这些方法导致线程会花很长时间卡在那或者永远卡在那。这时你会怎么办呢?为了让程序继续运行,避免卡在这种地方,你需要中断这种卡死的线程并抛出是哪里导致线程阻塞。所以某些方法就需要声明InterruptedException,表明这个方法会响应线程中断。而之前讲过中断线程可以用Thread.interrupt()或者使用人工的中断标志),最主要的几点是:

  • 捕获InterruptedException之后应该适当地对它进行处理-大多数情况下适当的处理指的是完全地跳出当前任务/循环/线程。
  • 吞掉InterruptedException不是一个好主意
  • 如果线程在非阻塞调用中被打断了,这时应该使用isInterrupted()。当线程早已被打断(也就是被标记为interrupted)时,一旦进入阻塞方法就应该立刻抛出InterruptedException。


相关文章

发表评论

Comment form

(*) 表示必填项

还没有评论。

跳到底部
返回顶部