线程在执行其run方法时,很有可能抛出异常。而run方法签名中,并未声明会抛出任何检查型异常。但在实际程序中,run方法中极其可能抛出一个异常,从而导致此线程被终止。更糟糕的是,如果线程因为异常终止,我们无法在主线程中使用try...catch...进行异常的捕获,从而可能导致一些问题的发生,例如无法释放某些资源等。主线程之所以不处理子线程抛出的RuntimeException,是因为线程是异步的,子线程没结束,主线程可能已经结束了。Thread类中的setUncaughtExceptionHandler就是处理线程中那些未捕获的异常,更明确的说,它处理那些未捕获的运行时异常。以下是一个例子,:
public class Dummy { public static void main(String[] args) { ThreadA threadA = null; ThreadB threadB = null; try { UncaughtException exe = new UncaughtException(); threadA = new ThreadA(); threadA.setName("threadA"); threadA.setUncaughtExceptionHandler(exe); threadA.start(); //threadA.run(); // exception thrown by common method can by caught in main thread } catch (Exception e) { System.out.println("catch RuntimeException e " + e.getMessage()); } // main thread can not caught the exception thrown in ThreadB try { threadB = new ThreadB(); threadB.start(); } catch (Exception e) { System.out.println("catch RuntimeException e " + e.getMessage()); // not effect } } } class UncaughtException implements UncaughtExceptionHandler{ @Override public void uncaughtException(Thread t, Throwable e) { System.out.println("thread " + t.getName() + " throws a uncaught excpetion " + e.getMessage()); e.printStackTrace(); } } class ThreadA extends Thread{ public void run(){ double a = 12 / 0; } } class ThreadB extends Thread{ public void run(){ try { double a = 12 / 0 ; } catch (ArithmeticException e) { throw e; } } }
程序的输出结果:
thread threadA throws a uncaught excpetion / by zero java.lang.ArithmeticException: / by zero at org.java.test1.ThreadA.run(Dummy.java:40) Exception in thread "Thread-1" java.lang.ArithmeticException: / by zero at org.java.test1.ThreadB.run(Dummy.java:48)
线程内部的处理机制是这样子的:当一个线程突然间被一个无法捕获的异常终止时,首先这个线程本身会处理这个异常,由jvm调用dispatchUncaughtException方法,查看线程是否设置了异常处理方法,如果没有为此线程设置异常处理方法,此时线程会查看此线程所在线程组是否设置了线程异常处理方法,将异常处理交给线程组,然后ThreadGroup的uncaughtException方法会处理这个异常(也由jvm调用),在这个方法里,ThreadGroup会首先判断这个线程组是否还有父线程组,如果有父线程组,则继续交由父线程组处理这个异常,如果不存在父线程组,其会调用这个线程的默认异常处理方法,如果这个线程有默认的异常处理策略,则用这个默认的异常处理策略进行异常的处理,否则的话,会判断这个异常是否属于ThreadDeath类型(extends Error),如果这个异常属于ThreadDeath类型,则放弃处理,否则打印这个异常信息。
同样,我们可以设置线程的默认异常处理策略,通过setDefaultUncaughtExceptionHandler即可,我们也可以通过调用getDefaultUncaughtExceptionHandler和getUncaughtExceptionHandler得到线程默认的异常处理程序信息和线程异常处理程序信息
3.1 InterruptedException异常
Thread.sleep()、 Thread.join() 或 Object.wait()都可以抛出InterruptedException,它是一个检查异常(checked exception)。当一个线程在wait set中等待或者调用了sleep方法,而另一个线程调用了interrupt方法中断当前线程时,就会抛出InterruptedException。
当一个方法抛出InterruptedException,表示这个方法是一个阻塞方法。
3.2 阻塞方法
阻塞的方法,不同于一般的普通方法。一般方法的完成只取决于它所要做的事情,以及是否有足够多可用的计算资源。而阻塞的方法还要取决于外部的一些事件,例如I/O完成,等待另一个线程释放对象锁等。一般方法在他们的工作做完后即可结束,而阻塞方法却不一定,其结束很难预测,因为他们还受外部因素的影响。
阻塞方法可能因为等不到外部的事件而无法结束,那么让阻塞方法可取消就非常有用。可取消是指能从外部使之在正常结束工作前终止的操作。由Thread 提供并受 Thread.sleep()和Object.wait()支持的中断机制就是一种取消机制;它允许一个线程请求另一个线程停止它正在做的事情。当一个方法抛出 InterruptedException时,它是在告诉您,如果执行该方法的线程被中断,它将尝试停止它正在做的事情而提前返回,并通过抛出InterruptedException表明它提前返回。
3.3 处理InterruptedException异常
如果一个方法抛出InterruptedException,表示这个方法为阻塞的方法,那么调用这个阻塞方法的方法也是一个阻塞方法。所以,我们必须有策略来处理InterruptedException异常。
3.3.1 将InterruptedException异常传递给调用者
public class BoundQueue { // private static final int MAX_COUNT = 1000; private BlockingQueue<Message> queue = new LinkedBlockingQueue<Message>(MAX_COUNT); // public void put(Message msg) throws InterruptedException{ queue.put(msg); } public Message take() throws InterruptedException{ return queue.take(); } }
3.3.2 在重新抛出InterruptedException异常前作特定的工作
有时候,我们必须在抛出InterruptedException异常前,做一些特定的工作,例如:当一个游戏需要两个人同时加入才可以开始,如果一个人到来,程序在等待第二个人到来前中断,此时我们需要将第一个人放回到队列中,然后抛出InterruptedException异常警告,才不会让第一个的请求丢失。
public class MatcherPlayer { // private final PlayerQueue queue = new PlayerQueue(2); private PlayerSource source; private Game game; private Player one; private Player two; public MatcherPlayer(PlayerSource source){ this.source = source; } public void matchPlayer() throws InterruptedException { try { while(true){ one = source.waitForOne(); two = source.waitForOne(); game.start(one, two); } } catch (InterruptedException e) { if(one != null){ queue.put(one); } throw e; } } }
3.3.3 捕捉 InterruptedException 后恢复中断状态
有时候抛出 InterruptedException 并不合适,例如当由 Runnable 定义的任务调用一个可中断的方法时,就是如此。
public class TaskRunner implements Runnable { // private BlockingQueue<Message> queue; public TaskRunner(BlockingQueue<Message> queue){ this.queue = queue; } public void run(){ try { while(true){ Message message = queue.take(); message.execute(); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } }
3.3.4 生吞中断
处理InterruptedException时采取的最糟糕的做法是生吞它 —— 捕捉它,然后既不重新抛出它,也不重新断言线程的中断状态。对于不知如何处理的异常,最标准的处理方法是捕捉它,然后记录下它,但是这种方法仍然无异于生吞中断,因为调用栈中更高层的代码还是无法获得关于该异常的信息。
public class TaskRunnerBadly implements Runnable { // private BlockingQueue<Message> queue; public TaskRunnerBadly(BlockingQueue<Message> queue){ this.queue = queue; } public void run(){ try { while(true){ Message message = queue.take(); message.execute(); } } catch (InterruptedException e) { // NOP } } }
3.4 不可中断的阻塞方法
并非所有的阻塞方法都抛出 InterruptedException。输入和输出流类会阻塞等待 I/O 完成,但是它们不抛出 InterruptedException,而且在被中断的情况下也不会提前返回。然而,对于套接字 I/O,如果一个线程关闭套接字,则那个套接字上的阻塞 I/O 操作将提前结束,并抛出一个 SocketException。java.nio 中的非阻塞 I/O 类也不支持可中断 I/O,但是同样可以通过关闭通道或者请求 Selector 上的唤醒来取消阻塞操作。类似地,尝试获取一个内部锁的操作(进入一个 synchronized 块)是不能被中断的,但是 ReentrantLock 支持可中断的获取模式。