Java处理InterruptedException异常的理论与实践

前言

对于 InterruptedException,一种常见的处理方式是 “生吞(swallow)” 它 —— 捕捉它,然后什么也不做(或者记录下它,不过这也好不到哪去)—— 就像后面的 清单 4 一样。不幸的是,这种方法忽略了这样一个事实:这期间可能发生中断,而中断可能导致应用程序丧失及时取消活动或关闭的能力。

阻塞方法

当一个方法抛出 InterruptedException 时,它不仅告诉您它可以抛出一个特定的检查异常,而且还告诉您其他一些事情。例如,它告诉您它是一个阻塞(blocking)方法,如果您响应得当的话,它将尝试消除阻塞并尽早返回。

阻塞方法不同于一般的要运行较长时间的方法。一般方法的完成只取决于它所要做的事情,以及是否有足够多可用的计算资源(CPU 周期和内存)。而阻塞方法的完成还取决于一些外部的事件,例如计时器到期,I/O 完成,或者另一个线程的动作(释放一个锁,设置一个标志,或者将一个任务放在一个工作队列中)。一般方法在它们的工作做完后即可结束,而阻塞方法较难于预测,因为它们取决于外部事件。阻塞方法可能影响响应能力,因为难于预测它们何时会结束。

阻塞方法可能因为等不到所等的事件而无法终止,因此令阻塞方法可取消 就非常有用(如果长时间运行的非阻塞方法是可取消的,那么通常也非常有用)。可取消操作是指能从外部使之在正常完成之前终止的操作。由 Thread 提供并受 Thread.sleep() 和 Object.wait() 支持的中断机制就是一种取消机制;它允许一个线程请求另一个线程停止它正在做的事情。当一个方法抛出 InterruptedException 时,它是在告诉您,如果执行该方法的线程被中断,它将尝试停止它正在做的事情而提前返回,并通过抛出 InterruptedException 表明它提前返回。 行为良好的阻塞库方法应该能对中断作出响应并抛出 InterruptedException,以便能够用于可取消活动中,而不至于影响响应。

线程中断

每个线程都有一个与之相关联的 Boolean 属性,用于表示线程的中断状态(interrupted status)。中断状态初始时为 false;当另一个线程通过调用 Thread.interrupt() 中断一个线程时,会出现以下两种情况之一。如果那个线程在执行一个低级可中断阻塞方法,例如 Thread.sleep()、 Thread.join() 或 Object.wait(),那么它将取消阻塞并抛出 InterruptedException。否则, interrupt() 只是设置线程的中断状态。 在被中断线程中运行的代码以后可以轮询中断状态,看看它是否被请求停止正在做的事情。中断状态可以通过 Thread.isInterrupted() 来读取,并且可以通过一个名为 Thread.interrupted() 的操作读取和清除。

中断是一种协作机制。当一个线程中断另一个线程时,被中断的线程不一定要立即停止正在做的事情。相反,中断是礼貌地请求另一个线程在它愿意并且方便的时候停止它正在做的事情。有些方法,例如 Thread.sleep(),很认真地对待这样的请求,但每个方法不是一定要对中断作出响应。对于中断请求,不阻塞但是仍然要花较长时间执行的方法可以轮询中断状态,并在被中断的时候提前返回。 您可以随意忽略中断请求,但是这样做的话会影响响应。

中断的协作特性所带来的一个好处是,它为安全地构造可取消活动提供更大的灵活性。我们很少希望一个活动立即停止;如果活动在正在进行更新的时候被取消,那么程序数据结构可能处于不一致状态。中断允许一个可取消活动来清理正在进行的工作,恢复不变量,通知其他活动它要被取消,然后才终止。

处理 InterruptedException

如果抛出 InterruptedException 意味着一个方法是阻塞方法,那么调用一个阻塞方法则意味着您的方法也是一个阻塞方法,而且您应该有某种策略来处理 InterruptedException。通常最容易的策略是自己抛出 InterruptedException,如清单 1 中 putTask() 和 getTask() 方法中的代码所示。 这样做可以使方法对中断作出响应,并且只需将 InterruptedException 添加到 throws 子句。

清单 1. 不捕捉 InterruptedException,将它传播给调用者

public class TaskQueue {
  private static final int MAX_TASKS = 1000;

  private BlockingQueue<Task> queue
    = new LinkedBlockingQueue<Task>(MAX_TASKS);

  public void putTask(Task r) throws InterruptedException {
    queue.put(r);
  }

  public Task getTask() throws InterruptedException {
    return queue.take();
  }
}

有时候需要在传播异常之前进行一些清理工作。在这种情况下,可以捕捉 InterruptedException,执行清理,然后抛出异常。清单 2 演示了这种技术,该代码是用于匹配在线游戏服务中的玩家的一种机制。 matchPlayers() 方法等待两个玩家到来,然后开始一个新游戏。如果在一个玩家已到来,但是另一个玩家仍未到来之际该方法被中断,那么它会将那个玩家放回队列中,然后重新抛出 InterruptedException,这样那个玩家对游戏的请求就不至于丢失。

清单 2. 在重新抛出 InterruptedException 之前执行特定于任务的清理工作

public class PlayerMatcher {
  private PlayerSource players;

  public PlayerMatcher(PlayerSource players) {
    this.players = players;
  }

  public void matchPlayers() throws InterruptedException {
    try {
       Player playerOne, playerTwo;
       while (true) {
         playerOne = playerTwo = null;
         // Wait for two players to arrive and start a new game
         playerOne = players.waitForPlayer(); // could throw IE
         playerTwo = players.waitForPlayer(); // could throw IE
         startNewGame(playerOne, playerTwo);
       }
     }
     catch (InterruptedException e) {
       // If we got one player and were interrupted, put that player back
       if (playerOne != null)
         players.addFirst(playerOne);
       // Then propagate the exception
       throw e;
     }
  }
}

不要生吞中断

有时候抛出 InterruptedException 并不合适,例如当由 Runnable 定义的任务调用一个可中断的方法时,就是如此。在这种情况下,不能重新抛出 InterruptedException,但是您也不想什么都不做。当一个阻塞方法检测到中断并抛出 InterruptedException 时,它清除中断状态。如果捕捉到 InterruptedException 但是不能重新抛出它,那么应该保留中断发生的证据,以便调用栈中更高层的代码能知道中断,并对中断作出响应。该任务可以通过调用 interrupt() 以 “重新中断” 当前线程来完成,如清单 3 所示。至少,每当捕捉到 InterruptedException 并且不重新抛出它时,就在返回之前重新中断当前线程。

清单 3. 捕捉 InterruptedException 后恢复中断状态

public class TaskRunner implements Runnable {
  private BlockingQueue<Task> queue;

  public TaskRunner(BlockingQueue<Task> queue) {
    this.queue = queue;
  }

  public void run() {
    try {
       while (true) {
         Task task = queue.take(10, TimeUnit.SECONDS);
         task.execute();
       }
     }
     catch (InterruptedException e) {
       // Restore the interrupted status
       Thread.currentThread().interrupt();
     }
  }
}

处理 InterruptedException 时采取的最糟糕的做法是生吞它 —— 捕捉它,然后既不重新抛出它,也不重新断言线程的中断状态。对于不知如何处理的异常,最标准的处理方法是捕捉它,然后记录下它,但是这种方法仍然无异于生吞中断,因为调用栈中更高层的代码还是无法获得关于该异常的信息。(仅仅记录 InterruptedException 也不是明智的做法,因为等到人来读取日志的时候,再来对它作出处理就为时已晚了。) 清单 4 展示了一种使用得很广泛的模式,这也是生吞中断的一种模式:

清单 4. 生吞中断 —— 不要这么做

// Don't do this
public class TaskRunner implements Runnable {
  private BlockingQueue<Task> queue;

  public TaskRunner(BlockingQueue<Task> queue) {
    this.queue = queue;
  }

  public void run() {
    try {
       while (true) {
         Task task = queue.take(10, TimeUnit.SECONDS);
         task.execute();
       }
     }
     catch (InterruptedException swallowed) {
       /* DON'T DO THIS - RESTORE THE INTERRUPTED STATUS INSTEAD */
     }
  }
}

如果不能重新抛出 InterruptedException,不管您是否计划处理中断请求,仍然需要重新中断当前线程,因为一个中断请求可能有多个 “接收者”。标准线程池 (ThreadPoolExecutor)worker 线程实现负责中断,因此中断一个运行在线程池中的任务可以起到双重效果,一是取消任务,二是通知执行线程线程池正要关闭。如果任务生吞中断请求,则 worker 线程将不知道有一个被请求的中断,从而耽误应用程序或服务的关闭。

实现可取消任务

语言规范中并没有为中断提供特定的语义,但是在较大的程序中,难于维护除取消外的任何中断语义。取决于是什么活动,用户可以通过一个 GUI 或通过网络机制,例如 JMX 或 Web 服务来请求取消。程序逻辑也可以请求取消。例如,一个 Web 爬行器(crawler)如果检测到磁盘已满,它会自动关闭自己,否则一个并行算法会启动多个线程来搜索解决方案空间的不同区域,一旦其中一个线程找到一个解决方案,就取消那些线程。

仅仅因为一个任务是可取消的,并不意味着需要立即 对中断请求作出响应。对于执行一个循环中的代码的任务,通常只需为每一个循环迭代检查一次中断。取决于循环执行的时间有多长,任何代码可能要花一些时间才能注意到线程已经被中断(或者是通过调用 Thread.isInterrupted() 方法轮询中断状态,或者是调用一个阻塞方法)。 如果任务需要提高响应能力,那么它可以更频繁地轮询中断状态。阻塞方法通常在入口就立即轮询中断状态,并且,如果它被设置来改善响应能力,那么还会抛出 InterruptedException。

惟一可以生吞中断的时候是您知道线程正要退出。只有当调用可中断方法的类是 Thread 的一部分,而不是 Runnable 或通用库代码的情况下,才会发生这样的场景,清单 5 演示了这种情况。清单 5 创建一个线程,该线程列举素数,直到被中断,这里还允许该线程在被中断时退出。用于搜索素数的循环在两个地方检查是否有中断:一处是在 while 循环的头部轮询 isInterrupted() 方法,另一处是调用阻塞方法 BlockingQueue.put()。

清单 5. 如果知道线程正要退出的话,则可以生吞中断

public class PrimeProducer extends Thread {
  private final BlockingQueue<BigInteger> queue;

  PrimeProducer(BlockingQueue<BigInteger> queue) {
    this.queue = queue;
  }

  public void run() {
    try {
      BigInteger p = BigInteger.ONE;
      while (!Thread.currentThread().isInterrupted())
        queue.put(p = p.nextProbablePrime());
    } catch (InterruptedException consumed) {
      /* Allow thread to exit */
    }
  }

  public void cancel() { interrupt(); }
}

不可中断的阻塞方法

并非所有的阻塞方法都抛出 InterruptedException。输入和输出流类会阻塞等待 I/O 完成,但是它们不抛出 InterruptedException,而且在被中断的情况下也不会提前返回。然而,对于套接字 I/O,如果一个线程关闭套接字,则那个套接字上的阻塞 I/O 操作将提前结束,并抛出一个 SocketException。java.nio 中的非阻塞 I/O 类也不支持可中断 I/O,但是同样可以通过关闭通道或者请求 Selector 上的唤醒来取消阻塞操作。类似地,尝试获取一个内部锁的操作(进入一个 synchronized 块)是不能被中断的,但是 ReentrantLock 支持可中断的获取模式。

不可取消的任务

有些任务拒绝被中断,这使得它们是不可取消的。但是,即使是不可取消的任务也应该尝试保留中断状态,以防在不可取消的任务结束之后,调用栈上更高层的代码需要对中断进行处理。清单 6 展示了一个方法,该方法等待一个阻塞队列,直到队列中出现一个可用项目,而不管它是否被中断。为了方便他人,它在结束后在一个 finally 块中恢复中断状态,以免剥夺中断请求的调用者的权利。(它不能在更早的时候恢复中断状态,因为那将导致无限循环 —— BlockingQueue.take() 将在入口处立即轮询中断状态,并且,如果发现中断状态集,就会抛出 InterruptedException。)

清单 6. 在返回前恢复中断状态的不可取消任务

public Task getNextTask(BlockingQueue<Task> queue) {
  boolean interrupted = false;
  try {
    while (true) {
      try {
        return queue.take();
      } catch (InterruptedException e) {
        interrupted = true;
        // fall through and retry
      }
    }
  } finally {
    if (interrupted)
      Thread.currentThread().interrupt();
  }
}

总结

您可以用 Java 平台提供的协作中断机制来构造灵活的取消策略。各活动可以自行决定它们是可取消的还是不可取消的,以及如何对中断作出响应,如果立即返回会危害应用程序完整性的话,它们还可以推迟中断。即使您想在代码中完全忽略中断,也应该确保在捕捉到 InterruptedException 但是没有重新抛出它的情况下,恢复中断状态,以免调用它的代码无法获知中断的发生。以上就是Java处理InterruptedException异常的理论与实践的全部内容,希望本文对大家有所帮助,如果有疑问欢迎大家留言进行讨论。

(0)

相关推荐

  • interrupt()和线程终止方式_动力节点Java学院整理

    中断线程 线程的thread.interrupt()方法是中断线程,将会设置该线程的中断状态位,即设置为true,中断的结果线程是死亡.还是等待新的任务或是继续运行至下一步,就取决于这个程序本身.线程会不时地检测这个中断标示位,以判断线程是否应该被中断(中断标示值是否为true).它并不像stop方法那样会中断一个正在运行的线程. 判断线程是否被中断 判断某个线程是否已被发送过中断请求,请使用Thread.currentThread().isInterrupted()方法(因为它将线程中断标示位

  • JAVA多线程之中断机制stop()、interrupted()、isInterrupted()

    一,介绍 本文记录JAVA多线程中的中断机制的一些知识点.主要是stop方法.interrupted()与isInterrupted()方法的区别,并从源代码的实现上进行简单分析. JAVA中有3种方式可以终止正在运行的线程 ①线程正常退出,即run()方法执行完毕了 ②使用Thread类中的stop()方法强行终止线程.但stop()方法已经过期了,不推荐使用 ③使用中断机制 线程正常退出没有什么东东,中断机制下面详细介绍,先看下stop()方法的源代码,关键是源代码上的注释.它解释了为什么s

  • Java interrupt()方法使用注意_动力节点Java学院整理

    程序是很简易的.然而,在编程人员面前,多线程呈现出了一组新的难题,如果没有被恰当的解决,将导致意外的行为以及细微的.难以发现的错误. 在本篇文章中,我们针对这些难题之一:如何中断一个正在运行的线程. 背景 中断(Interrupt)一个线程意味着在该线程完成任务之前停止其正在进行的一切,有效地中止其当前的操作.线程是死亡.还是等待新的任务或是继续运行至下一步,就取决于这个程序.虽然初次看来它可能显得简单,但是,你必须进行一些预警以实现期望的结果.你最好还是牢记以下的几点告诫. 首先,忘掉Thre

  • Java多线程之中断线程(Interrupt)的使用详解

    interrupt方法 interrupt字面上是中断的意思,但在Java里Thread.interrupt()方法实际上通过某种方式通知线程,并不会直接中止该线程.具体做什么事情由写代码的人决定,通常我们会中止该线程. 如果线程在调用Object类的wait().wait(long)或wait(long, int)方法,或者该类的 join() .join(long) .join(long, int) .sleep(long) 或 sleep(long, int) 方法过程中受阻,则其中断状态

  • Java处理InterruptedException异常的理论与实践

    前言 对于 InterruptedException,一种常见的处理方式是 "生吞(swallow)" 它 -- 捕捉它,然后什么也不做(或者记录下它,不过这也好不到哪去)-- 就像后面的 清单 4 一样.不幸的是,这种方法忽略了这样一个事实:这期间可能发生中断,而中断可能导致应用程序丧失及时取消活动或关闭的能力. 阻塞方法 当一个方法抛出 InterruptedException 时,它不仅告诉您它可以抛出一个特定的检查异常,而且还告诉您其他一些事情.例如,它告诉您它是一个阻塞(bl

  • java高并发InterruptedException异常引发思考

    目录 前言 程序案例 问题分析 问题解决 总结 前言 InterruptedException异常可能没你想的那么简单! 当我们在调用Java对象的wait()方法或者线程的sleep()方法时,需要捕获并处理InterruptedException异常.如果我们对InterruptedException异常处理不当,则会发生我们意想不到的后果! 程序案例 例如,下面的程序代码,InterruptedTask类实现了Runnable接口,在run()方法中,获取当前线程的句柄,并在while(t

  • JAVA中常见异常类

    1. java.lang.nullpointerexception 这个异常大家肯定都经常遇到,异常的解释是"程序遇上了空指针",简单地说就是调用了未经初始化的对象或者是不存在的对象,这个错误经常出现在创建图片,调用数组这些操作中,比如图片未经初始化,或者图片创建时的路径错误等等.对数组操作中出现空指针,很多情况下是一些刚开始学习编程的朋友常犯的错误,即把数组的初始化和数组元素的初始化混淆起来了.数组的初始化是对数组分配需要的空间,而初始化后的数组,其中的元素并没有实例化,依然是空的,

  • Java程序常见异常及处理汇总

    一.JDK中常见的异常情况 1.常见异常总结图 2.java中异常分类 Throwable类有两个直接子类: (1)Exception:出现的问题是可以被捕获的 (2)Error:系统错误,通常由JVM处理 3.被捕获的异常分类 (1)Check异常: 派生自Exception的异常类,必须被捕获或再次声明抛出 (2)Runtime异常:派生自RuntimeException的异常类.使用throw语句可以随时抛出这种异常对象 throw new ArithmeticException(-);

  • 详解Java的Exception异常机制

    一.前言 在Java中,我们在执行代码的过程中难免会遇到错误与Exception异常,可是我们一直都是锤头Coding而忽略了学习Exception这个东西!我们只是知道在发生Exception的地方让代码自动生成throw exception或者是使用try-catch括起来处理,那你了解Java的Exception吗?今天就让我们把一起来看看Java的Exception吧! 在Java中,我们的代码再出现错误的时候无非是两种情况:一是Error,一是异常Exception.如果是Error,

  • Java 详解异常的处理机制

    目录 1.异常概述与异常体系结构 1.1异常概述 1.2运行时异常与编译时异常 1.3异常体系结构 2.常见异常 1. ArrayIndexOutOfBoundsException 2.NullPointerException 3.ArithmeticException 4.ClassCastException 3.异常处理机制 3.1异常的抛出与捕获 3.2异常处理机制:try-catch-finally 5.用户自定义异常类 6.异常处理5个关键字 1.异常概述与异常体系结构 1.1异常概述

  • 详解Java子线程异常时主线程事务如何回滚

    一.提出问题 最近有一位朋友问了我这样一个问题,问题的截图如下: 这个问题问的相对比较笼统,我来稍微详细的描述下:主线程向线程池提交了一个任务,如果执行这个任务过程中发生了异常,如何让主线程捕获到该异常并且进行事务的回滚. 二.主线程与子线程 先来看看基础,下图体现了两种线程的运行方式, 左侧的图,体现了主线程启动一个子线程之后,二者互不干扰独立运行,生死有命,从此你我是路人! 右侧的图,体现了主线程启动一个子线程之后继续执行主线程程序逻辑,在某一节点通过阻塞的方式来获取子线程的执行结果. 对于

  • 详解Java中多线程异常捕获Runnable的实现

    详解Java中多线程异常捕获Runnable的实现 1.背景: Java 多线程异常不向主线程抛,自己处理,外部捕获不了异常.所以要实现主线程对子线程异常的捕获. 2.工具: 实现Runnable接口的LayerInitTask类,ThreadException类,线程安全的Vector 3.思路: 向LayerInitTask中传入Vector,记录异常情况,外部遍历,判断,抛出异常. 4.代码: package step5.exception; import java.util.Vector

  • Java中内存异常StackOverflowError与OutOfMemoryError详解

     Java中内存异常StackOverflowError与OutOfMemoryError详解 使用Java开发,经常回遇到内存异常的情况,而StackOverflowError和OutOfMemoryError便是最常遇见的错误. 首先,看看这两种错误的解释: 如果当前线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常. 如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常. 这里把异常分为两种情况,但是存在一些相互重

  • 深入探讨JAVA中的异常与错误处理

    异常与错误: 异常: 在Java中程序的错误主要是语法错误和语义错误,一个程序在编译和运行时出现的错误我们统一称之为异常,它是VM(虚拟机)通知你的一种方式,通过这种方式,VM让你知道,你(开发人员)已经犯了个错误,现在有一个机会来修改它.Java中使用异常类来表示异常,不同的异常类代表了不同的异常.但是在Java中所有的异常都有一个基类,叫做Exception. 错误: 它指的是一个合理的应用程序不能截获的严重的问题.大多数都是反常的情况.错误是VM的一个故障(虽然它可以是任何系统级的服务).

随机推荐