Java多线程异常捕获之UncaughtExceptionHandler

Thread和UncaughtExceptionHandler

相信现在大家在写java程序时,必然会接触到多线程的概念。多线程很强大,但是也很容易出错。其中一个经常被忽略的错误就是线程无故异常退出,这种情况多数是因为某些未被捕获的异常直接抛出导致的,而且这时候如果处理不当,可能会导致系统资源的泄露,比如数据库连接未释放等。

先看下面的例子:

1
2
3
4
5
6
7
8
9
Runnable runnable = new Runnable() {
@Override
public void run() {
int a = 1 / 0;
System.out.println(a);
}
};

new Thread(runnable).start();

上面的代码中,执行到1/0的时候必然会抛出异常,导致下面的语句没法执行。虽然run接口不抛出任何受检异常,但是确可能抛出未受检异常从而导致执行线程的退出。为了防止线程无声的退出,我们可以在代码中用try{...}catch(Throwable t){}的方法来讲所有的异常捕获。另一种方法是利用Thread提供的UncaughtExceptionHandler来处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Runnable runnable = new Runnable() {
@Override
public void run() {
int a = 1 / 0;
System.out.println(a);
}
};

Thread thread = new Thread(runnable);
thread.setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
@Override
public void uncaughtException(Thread t, Throwable e) {
System.out.println(String.format("Exception got - doing something, thread=%s, errMsg=%s", t.getName(), e.getMessage()));
}
});
thread.start();

执行结果:

1
Exception got - doing something, thread=Thread-0, errMsg=/ by zero

在上面的代码中,我们为线程指定了一个Handler来处理未被捕获的异常,这种做法只会对该线程有限,其他的线程抛出的未受检异常则不会被处理。Thread中可以设置一个默认的UncaughtExceptionHandler,这样可以将该异常处理句柄应用到大部分的线程上。

1
2
3
4
5
6
Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
@Override
public void uncaughtException(Thread t, Throwable e) {
// do something
}
});

Executor框架和UncaughtExceptionHandler

使用Thread.setDefaultUncaughtExceptionHandler

Executor框架是最常用的一个线程框架,那么在线程池中,如果线程抛出的异常未被捕获,同样会导致工作线程的退出,线程池会根据情况,确定是否起新的线程来代替该工作线程。前面提到的通过Thread.setDefaultUncaughtExceptionHandler同样对通过线程池创建的线程起作用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Runnable runnable = new Runnable() {
@Override
public void run() {
int a = 1 / 0;
System.out.println(a);
}
};

Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
@Override
public void uncaughtException(Thread t, Throwable e) {
System.out.println("Uncaught exception, thread=" + t.getName() + ", exception=" + e.getMessage());
}
});

ExecutorService executorService = Executors.newSingleThreadExecutor();
for (int i = 0; i < 2; i++) {
executorService.execute(runnable);
}

执行结果:

1
2
Uncaught exception, thread=pool-1-thread-1, exception=/ by zero
Uncaught exception, thread=pool-1-thread-2, exception=/ by zero

利用ThreadFactory

还有一种方法就是利用ThreadFactory,我们在创建线程池的时候指定一个ThreadFactory,该工厂负责为每个由其创建线程设置UncaughtExceptionHandler。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("Start");
int a = 1 / 0;
System.out.println(a);
}
};

Thread.UncaughtExceptionHandler uncaughtExceptionHandler = new Thread.UncaughtExceptionHandler() {
@Override
public void uncaughtException(Thread t, Throwable e) {
System.out.println("Uncaught exception, thread=" + t.getName() + ", exception=" + e.getMessage());
}
};

AtomicInteger threadCount = new AtomicInteger();
ExecutorService executorService = Executors.newSingleThreadExecutor(new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
Thread thread = new Thread(r, "thread-" + threadCount.incrementAndGet());
thread.setUncaughtExceptionHandler(uncaughtExceptionHandler);
return thread;
}
});
for (int i = 0; i < 2; i++) {
executorService.execute(runnable);
}

执行结果:

1
2
3
4
Start
Uncaught exception, thread=pool-1-thread-1, exception=/ by zero
Start
Uncaught exception, thread=pool-1-thread-2, exception=/ by zero

请注意submit(Runnable)

但是,即使你设置了UncaughtExceptionHandler,在线程池中也可能会遇到异常未被处理的情况。我们把上面的代码中线程池execute()改为submit()再执行一次:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("Start");
int a = 1 / 0;
System.out.println(a);
}
};

Thread.UncaughtExceptionHandler uncaughtExceptionHandler = new Thread.UncaughtExceptionHandler() {
@Override
public void uncaughtException(Thread t, Throwable e) {
System.out.println("Uncaught exception, thread=" + t.getName() + ", exception=" + e.getMessage());
}
};

AtomicInteger threadCount = new AtomicInteger();
ExecutorService executorService = Executors.newSingleThreadExecutor(new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
Thread thread = new Thread(r, "thread-" + threadCount.incrementAndGet());
thread.setUncaughtExceptionHandler(uncaughtExceptionHandler);
return thread;
}
});
for (int i = 0; i < 2; i++) {
executorService.execute(runnable);
}

输出:

1
2
Start
Start

可以发现,这时候异常并没有被UncaughtExceptionHandler处理。这是因为对于submit执行的任务,task产生的未处理的异常都会存在Future对象中作为执行的一种结果。我们可以通过Future#get(),如果有异常会被封装成ExecutionException抛出来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("Start");
int a = 1 / 0;
System.out.println("End");
}
};

Thread.UncaughtExceptionHandler uncaughtExceptionHandler = new Thread.UncaughtExceptionHandler() {
@Override
public void uncaughtException(Thread t, Throwable e) {
System.out.println("Uncaught exception, thread=" + t.getName() + ", exception=" + e.getMessage());
}
};

AtomicInteger threadCount = new AtomicInteger();
ExecutorService executorService = Executors.newSingleThreadExecutor(new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
Thread thread = new Thread(r, "thread-" + threadCount.incrementAndGet());
thread.setUncaughtExceptionHandler(uncaughtExceptionHandler);
return thread;
}
});
for (int i = 0; i < 2; i++) {
Future future = executorService.submit(runnable);
try {
future.get();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
System.out.println("Exception from future " + e.getCause().getMessage());
}
}

运行结果:

1
2
3
4
Start
Exception from future / by zero
Start
Exception from future / by zero

ScheduledExecutorService的execute和submit

这里还有个特例,ScheduledExecutorService的execute方法其实也是调用的submit,所以就算你调用ScheduledExecutorService#submit,任务中抛出的异常也不会走到UncaughtExceptionHandler中去。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// ScheduledThreadPoolExecutor#execute的源码
/**
* Executes {@code command} with zero required delay.
* This has effect equivalent to
* {@link #schedule(Runnable,long,TimeUnit) schedule(command, 0, anyUnit)}.
* Note that inspections of the queue and of the list returned by
* {@code shutdownNow} will access the zero-delayed
* {@link ScheduledFuture}, not the {@code command} itself.
*
* <p>A consequence of the use of {@code ScheduledFuture} objects is
* that {@link ThreadPoolExecutor#afterExecute afterExecute} is always
* called with a null second {@code Throwable} argument, even if the
* {@code command} terminated abruptly. Instead, the {@code Throwable}
* thrown by such a task can be obtained via {@link Future#get}.
*
* @throws RejectedExecutionException at discretion of
* {@code RejectedExecutionHandler}, if the task
* cannot be accepted for execution because the
* executor has been shut down
* @throws NullPointerException {@inheritDoc}
*/
public void execute(Runnable command) {
schedule(command, 0, NANOSECONDS);
}

总结

线程的异常退出可能会导致一些诡异的错误,应该尽量保证异常都被处理到。UncaughtExceptionHandler是一种有效的手段,但是要注意在和Executor框架结合是,submit的task产生的未捕获的异常不会被注册的UncaughtExceptionHandler处理,需要通过Future#get来处理。需要注意的是ScheduledExecutorService#execute其实是调用的submit方法。

JVM问题定位的瑞士军刀——JCMD 虚拟内存及监控方式

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×