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 支持可中断的获取模式。

猜你喜欢

转载自technoboy.iteye.com/blog/1038536