线程池吃了你的异常?
下面主要从线程池的两个提交任务的方法角度讲一下这个问题。
ExecutorService 的 submit 和 execute 两个方法的区别
先来看两段使用 submit 的代码
代码 1
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
public class FinallyTest {
private static Object str = null;
public static void main(String[] args) {
TestRun testRun = new TestRun();
ExecutorService executor = Executors.newCachedThreadPool();
Future<?> future = executor.submit(testRun);
try {
//future.get(5, TimeUnit.SECONDS);
}catch (Throwable t){
System.out.println("run 方法的异常被 主线程 捕获了。");
t.printStackTrace();
}
System.out.println("main方法结束");
}
public static class TestRun implements Runnable {
public void run() {
System.out.println(str.toString());
}
}
}
运行结果:
main方法结束
代码 2
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
public class FinallyTest {
private static Object str = null;
public static void main(String[] args) {
TestRun testRun = new TestRun();
ExecutorService executor = Executors.newCachedThreadPool();
Future<?> future = executor.submit(testRun);
try {
future.get(5, TimeUnit.SECONDS);
}catch (Throwable t){
System.out.println("run 方法的异常被 主线程 捕获了。");
t.printStackTrace();
}
System.out.println("main方法结束");
}
public static class TestRun implements Runnable {
public void run() {
System.out.println(str.toString());
}
}
}
运行结果:
run 方法的异常被 主线程 捕获了。
java.util.concurrent.ExecutionException: java.lang.NullPointerException
at java.util.concurrent.FutureTask.report(FutureTask.java:122)
at java.util.concurrent.FutureTask.get(FutureTask.java:206)
at com.zph.study.base.jvm.FinallyTest.main(FinallyTest.java:16)
Caused by: java.lang.NullPointerException
at com.zph.study.base.jvm.FinallyTest$TestRun.run(FinallyTest.java:27)
at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511)
at java.util.concurrent.FutureTask.run$$$capture(FutureTask.java:266)
at java.util.concurrent.FutureTask.run(FutureTask.java)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:748)
main方法结束
原因是什么呢?
因为调用 submit 之后,run 方法抛出的异常被线程池捕获并封装到了 Future 的 get 方法上。代码 1
并没有调用 Future.get ,这样异常只是被捕获了,并没有进一步抛出来,就像被线程池吃掉了一样。而 代码 2
调用了 Future.get ,get 方法将 run 方法抛出的异常进一步抛给了调用 get 方法的地方,所以这里就能够打印出 run 方法的异常信息了。
再看两段使用 submit 的代码
代码 3
import lombok.SneakyThrows;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
public class FinallyTest {
private static Object str = null;
public static void main(String[] args) {
TestRun testRun = new TestRun();
ExecutorService executor = Executors.newCachedThreadPool();
Future<?> future = executor.submit(testRun);
try {
//future.get(5, TimeUnit.SECONDS);
}catch (Throwable t){
System.out.println("run 方法的异常被 主线程 捕获了。");
t.printStackTrace();
}
System.out.println("main方法结束");
}
public static class TestRun implements Runnable {
@SneakyThrows
public void run() {
try {
System.out.println(str.toString());
}catch (Exception e){
System.out.println("run 方法的 catch 输出。");
e.printStackTrace();
System.out.println("run 方法结束,并抛出异常");
throw e;
}
}
}
}
运行结果:
main方法结束
run 方法的 catch 输出。
java.lang.NullPointerException
at com.zph.study.base.jvm.FinallyTest$TestRun.run(FinallyTest.java:30)
at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511)
at java.util.concurrent.FutureTask.run$$$capture(FutureTask.java:266)
at java.util.concurrent.FutureTask.run(FutureTask.java)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:748)
run 方法结束,并抛出异常
代码 4
import lombok.SneakyThrows;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
public class FinallyTest {
private static Object str = null;
public static void main(String[] args) {
TestRun testRun = new TestRun();
ExecutorService executor = Executors.newCachedThreadPool();
Future<?> future = executor.submit(testRun);
try {
future.get(5, TimeUnit.SECONDS);
}catch (Throwable t){
System.out.println("run 方法的异常被 主线程 捕获了。");
t.printStackTrace();
}
System.out.println("main方法结束");
}
public static class TestRun implements Runnable {
@SneakyThrows
public void run() {
try {
System.out.println(str.toString());
}catch (Exception e){
System.out.println("run 方法的 catch 输出。");
e.printStackTrace();
System.out.println("run 方法结束,并抛出异常");
throw e;
}
}
}
}
运行结果:
run 方法的 catch 输出。
java.lang.NullPointerException
at com.zph.study.base.jvm.FinallyTest$TestRun.run(FinallyTest.java:30)
at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511)
at java.util.concurrent.FutureTask.run$$$capture(FutureTask.java:266)
at java.util.concurrent.FutureTask.run(FutureTask.java)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:748)
run 方法结束,并抛出异常
run 方法的异常被 主线程 捕获了。
java.util.concurrent.ExecutionException: java.lang.NullPointerException
at java.util.concurrent.FutureTask.report(FutureTask.java:122)
at java.util.concurrent.FutureTask.get(FutureTask.java:206)
at com.zph.study.base.jvm.FinallyTest.main(FinallyTest.java:17)
Caused by: java.lang.NullPointerException
at com.zph.study.base.jvm.FinallyTest$TestRun.run(FinallyTest.java:30)
at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511)
at java.util.concurrent.FutureTask.run$$$capture(FutureTask.java:266)
at java.util.concurrent.FutureTask.run(FutureTask.java)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:748)
main方法结束
原因是什么呢?
从 代码 3
的运行结果我们可以看到,当 run 方法发生异常时,主线程已经结束了。而 代码 4
的运行结果则恰好相反,run 方法结束并抛出异常之后,主线程才捕获到异常,然后主线程结束。这就说明了 Future.get 会阻塞调用者的线程,直到通过 submit 提交的任务的 run 方法执行结束之后才会放开调用 Future.get 的线程。这也就是说,Future.get 将两个并行执行的线程变成了顺序执行。所以与其说线程池是将 run 方法抛出的异常捕获并封装到了 Future 的 get 方法上。倒不如说是调用了 Future.get 之后,调用者线程就一直在等待 run 方法执行完毕,如果 run 方法抛出了异常,就经由 Future.get 方法传递给调用者。
再来看 2 段使用 execute 的代码
代码 5
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class FinallyTest {
private static Object str = null;
public static void main(String[] args) {
TestRun testRun = new TestRun();
ExecutorService executor = Executors.newCachedThreadPool();
executor.execute(testRun);
try {//这个 try 代码块显得很无用,但我放在这里只是想和下边的代码做一个对照
}catch (Throwable t){
System.out.println("run 方法的异常被 主线程 捕获了。");
t.printStackTrace();
}
System.out.println("main方法结束");
}
public static class TestRun implements Runnable {
public void run() {
System.out.println(str.toString());
}
}
}
运行结果:
main方法结束
Exception in thread "pool-1-thread-1" java.lang.NullPointerException
at com.zph.study.base.jvm.FinallyTest$TestRun.run(FinallyTest.java:24)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:748)
代码6
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class FinallyTest {
private static Object str = null;
public static void main(String[] args) {
TestRun testRun = new TestRun();
ExecutorService executor = Executors.newCachedThreadPool();
try {
executor.execute(testRun);
}catch (Throwable t){
System.out.println("run 方法的异常被 主线程 捕获了。");
t.printStackTrace();
}
System.out.println("main方法结束");
}
public static class TestRun implements Runnable {
public void run() {
System.out.println(str.toString());
}
}
}
运行结果:
main方法结束
Exception in thread "pool-1-thread-1" java.lang.NullPointerException
at com.zph.study.base.jvm.FinallyTest$TestRun.run(FinallyTest.java:24)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:748)
原因是什么呢?
可以看到这两段代码的执行结果完全一样,即使把 execute 的调用放在 try 块中,run 方法抛出的异常也不会被这里的 catch 块捕获到。原因是调用 execute 方法时,线程池并不会对 run 方法抛出的异常进行捕获和处理,异常直接被抛给了 jvm ,jvm 就自己打印出来了。而且放入线程池中执行的 run 方法是异步执行的,很可能在主线程结束的时候,run 方法才刚刚抛出异常,这就更不可能被主线程捕获了。
总结
submit 和 execute 的区别
ExecutorService 的 submit 和 execute 两个方法刚好互补。
-
submit 是无法自动打印异常,因为它将 run 方法抛出的异常捕获后封装到了 Future 的 get 方法上
在主线程中调用 Future.get 就能捕获到 run 方法中抛出的异常
- Future.get 会阻塞调用者的线程,直到通过 submit 提交的任务的 run 方法执行结束之后才会放开调用 Future.get 的线程。这也就是说,Future.get 将两个并行执行的线程变成了顺序执行。所以与其说线程池是将 run 方法抛出的异常捕获并封装到了 Future 的 get 方法上。倒不如说是调用了 Future.get 之后,调用者线程就一直在等待 run 方法执行完毕,如果 run 方法抛出了异常,就经由 Future.get 方法传递给调用者。
- 如果不调用 Future.get 的话,run 方法抛出的异常就像被线程池吃掉了一样,有时候会让你摸不着头脑。明明 run方法抛出异常了,而你的控制台却没有任何异常打印,原因就出在了这里。
public interface Future<V> { /** * * @throws CancellationException if the computation was cancelled * @throws ExecutionException if the computation threw an * exception * @throws InterruptedException if the current thread was interrupted * while waiting * @throws TimeoutException if the wait timed out */ V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException; }
-
execute 恰好相反,它在 run 方法抛出异常后直接交给 jvm 打印了,由于 run 方法是异步方法,所以在
主线程中就无法捕获到 run 方法抛出的异常了
本人属于菜鸟一个,如果有哪些地方写的不对的,希望各位大佬予以指正,谢谢了。