Java Concurrent Programming (3)

3 线程异常
线程在执行其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 支持可中断的获取模式。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值