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。
  • 转载自 并发编程网 - ifeve.com
时间: 2024-09-02 19:46:22

InterruptedException 和 interrupting threads 的一些说明的相关文章

ExecutorService-10个要诀和技巧

ExecutorService抽象概念自Java5就已经提出来了,现在是2014年.顺便提醒一下:Java5和Java6都已不被支持,Java7在半年内也将会这样.我提出这个的原因是许多Java程序员仍然不能完全明白ExecutorService到底是怎样工作的.还有很多地方要去学习,今天我会分享一些很少人知道的特性和实践.然而这篇文章仍然是面向中等程序员的,没什么特别高级的地方. 1. Name pool threads 我想强调一点的是,当在运行JVM或调试期间创建线程时,默认的线程池命名规

CompletableFuture 不能被中断

我之前写过一篇关于InterruptedException and interrupting threads的文章.总之,如果你调用Future.cancel(),那么Future不仅会终止正在等待的get(),还会试图去中断底层的线程.这是个很重要的特征,它能够使线程池变得更加利于使用.我在之前的文章中也说过,相对于标准的Future,尽量使用CompletableFuture.但事实证明,Future的更加强大的兄弟-CompletableFuture并不能优雅地处理cancel(). 请思

Adding Multithreading Capability to Your Java Applications

src: http://www.informit.com/articles/article.aspx?p=26326&seqNum=3 Interrupting Threads A thread terminates when its run method returns. (In the first version of the Java programming environment, there also was astop method that another thread could

Java JUC之Atomic系列12大类实例讲解和原理分解

在java6以后我们不但接触到了Lock相关的锁,也接触到了很多更加乐观的原子修改操作,也就是在修改时我们只需要保证它的那个瞬间是安全的即可,经过相应的包装后可以再处理对象的并发修改,以及并发中的ABA问题,本文讲述Atomic系列的类的实现以及使用方法,其中包含: 基本类:AtomicInteger.AtomicLong.AtomicBoolean: 引用类型:AtomicReference.AtomicReference的ABA实例.AtomicStampedRerence.AtomicMa

从Java视角理解系统结构(三)伪共享

从Java视角理解系统结构连载, 关注我的微博(链接)了解最新动态 从我的前一篇博文中, 我们知道了CPU缓存及缓存行的概念, 同时用一个例子说明了编写单线程Java代码时应该注意的问题. 下面我们讨论更为复杂, 而且更符合现实情况的多核编程时将会碰到的问题. 这些问题更容易犯, 连j.u.c包作者Doug Lea大师的JDK代码里也存在这些问题. MESI协议及RFO请求 从前一篇我们知道, 典型的CPU微架构有3级缓存, 每个核都有自己私有的L1, L2缓存. 那么多线程编程时, 另外一个核

JDK5中的线程池

  JDK5中的一个亮点就是将Doug Lea的并发库引入到Java标准库中.Doug Lea确实是一个牛人,能教书,能出书,能编码,不过这在国外还是比较普遍的,而国内的教授们就相差太远了. 一般的服务器都需要线程池,比如Web.FTP等服务器,不过它们一般都自己实现了线程池,比如以前介绍过的Tomcat.Resin和Jetty等,现在有了JDK5,我们就没有必要重复造车轮了,直接使用就可以,何况使用也很方便,性能也非常高.   package concurrent; import java.u

Threads

7) Threads Objective 1) Write code to define, instantiate and start new threads using both java.lang.Thread and java.lang.Runnable ·    Java is fundamentally multi-threaded.·    Every thread corresponds to an instance of java.lang.Thread class or a s

Threads and Anonymous Classes in JAVA

As we all know,a thread is a separate process on your computer.you can run multiple threads all at the same time. multi-threaded code has the disadvantage of becoming quite complex very quickly,although java has some great classes for dealing with mu

Java8并发教程:Threads和Executors

欢迎阅读我的Java8并发教程的第一部分.这份指南将会以简单易懂的代码示例来教给你如何在Java8中进行并发编程.这是一系列教程中的第一部分.在接下来的15分钟,你将会学会如何通过线程,任务(tasks)和 exector services来并行执行代码. 第一部分:Threads和Executors 第二部分:同步和锁 并发在Java5中首次被引入并在后续的版本中不断得到增强.在这篇文章中介绍的大部分概念同样适用于以前的Java版本.不过我的代码示例聚焦于Java8,大量使用lambda表达式