看下面的例子:
public class DivTask implements Runnable { int a,b; public DivTask(int a,int b){ this.a = a; this.b = b; } @Override public void run() { double re = a / b; System.out.println(re); } //测试 public static void main(String[] args){ ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(0,Integer.MAX_VALUE,0L, TimeUnit.SECONDS,new SynchronousQueue<Runnable>()); for (int i = 0;i < 5;i++){ poolExecutor.submit(new DivTask(100,i)); } } }上述代码是将DivTask提交到线程池,从第16行for循环来看,我们会得到5个结果,分别是100除以i的商,下面就是这段代码的输出结果:
100.0 25.0 33.0 50.0 你没有看错,就只有4个结果,也就是说程序漏算了一组数据,但是更加不幸的是,没有任何的错误提示,就好像一切正常一样。但是在这个简单的案例中,只要你稍有经验,就能发现,作为除数i取到了0,这个缺失的值很可能是由于这个0导致的,但是如果是在稍微复杂的业务场景中,这种简单的错误足以让你几天萎靡不振。
也就是说:使用线程池虽然是件好事,但是得处处留意坑。线程池很可能会“吃”掉程序抛出的异常,导致我们对程序的错误一无所知。
改正方法: 1 最简单的方法,弃用submit(),改用execute()方法 将上述代码第17行修改为:
poolExecutor.execute(new DivTask(100,i)); 这样执行代码后,你将得到部分堆栈信息,执行结果如下:
复制代码 100.0 50.0 33.0 25.0 Exception in thread “pool-1-thread-1” java.lang.ArithmeticException: / by zero at CurrentJava.DivTask.run(DivTask.java:10) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617) at java.lang.Thread.run(Thread.java:745) 复制代码 注意了这里说的部分堆栈信息。这是因为从这两个异常堆栈中我们只知道异常在哪里抛出的(这里说的是第10行);但是我们还希望得到另外一个更重要的信息,那就是这个任务是在哪里提交的?而任务的具体提交位置已经被线程池给完全淹没了,顺着堆栈,我们最多只能找到线程调度的调度流程,而这对于我们来说几乎没有价值。
2 改造submit()方法 将上述代码第17行修改为:
Future re = poolExecutor.submit(new DivTask(100,i)); re.get(); 执行后得到输出结果:
复制代码 Exception in thread “main” java.util.concurrent.ExecutionException: java.lang.ArithmeticException: / by zero at java.util.concurrent.FutureTask.report(FutureTask.java:122) at java.util.concurrent.FutureTask.get(FutureTask.java:192) at CurrentJava.DivTask.main(DivTask.java:17) Caused by: java.lang.ArithmeticException: / by zero at CurrentJava.DivTask.run(DivTask.java:10) at java.util.concurrent.Executors R u n n a b l e A d a p t e r . c a l l ( E x e c u t o r s . j a v a : 511 ) a t j a v a . u t i l . c o n c u r r e n t . F u t u r e T a s k . r u n ( F u t u r e T a s k . j a v a : 266 ) a t j a v a . u t i l . c o n c u r r e n t . T h r e a d P o o l E x e c u t o r . r u n W o r k e r ( T h r e a d P o o l E x e c u t o r . j a v a : 1142 ) a t j a v a . u t i l . c o n c u r r e n t . T h r e a d P o o l E x e c u t o r RunnableAdapter.call(Executors.java:511) at java.util.concurrent.FutureTask.run(FutureTask.java:266) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142) at java.util.concurrent.ThreadPoolExecutor RunnableAdapter.call(Executors.java:511)atjava.util.concurrent.FutureTask.run(FutureTask.java:266)atjava.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)atjava.util.concurrent.ThreadPoolExecutorWorker.run(ThreadPoolExecutor.java:617) at java.lang.Thread.run(Thread.java:745) 复制代码 可以看出得到的堆栈信息与上面使用execute()方法几乎一致,都只能知道异常在哪里抛出的。
3 扩展 ThreadPoolExecutor 我们扩展ThreadPoolExecutor 线程池,让它在调度任务之前,先保存一下提交任务线程的堆栈信息,如下所示:
复制代码
public class TraceThreadPoolExecutor extends ThreadPoolExecutor { public TraceThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) { super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue); } @Override public void execute(Runnable task) { super.execute(wrap(task,clientTrace(),Thread.currentThread().getName())); } @Override public Future<?> submit(Runnable task) { return super.submit(wrap(task,clientTrace(),Thread.currentThread().getName())); } private Exception clientTrace(){ return new Exception("Client stack trace!"); } private Runnable wrap(final Runnable task,final Exception clientStack,String clientThreadName){ return new Runnable() { @Override public void run() { try { task.run(); }catch (Exception e){ clientStack.printStackTrace(); throw e; } } }; } }上述代码第21行,wrap()方法的第二个参数为一个异常,里面保存着提交任务的线程堆栈信息。该方法将我们传入的Runnable对象进行一层包装,使之能处理异常信息,当任务发生异常时,这个异常就会被打印(第28行)。
将第一个例子的main方法修改为:
复制代码
//测试 public static void main(String[] args){ ThreadPoolExecutor poolExecutor = new TraceThreadPoolExecutor(0,Integer.MAX_VALUE,0L,TimeUnit.SECONDS,new SynchronousQueue<Runnable>()); for (int i =0;i < 5;i++){ poolExecutor.execute(new DivTask(100,i)); } }执行,就可以得到下面的信息:
java.lang.Exception: Client stack trace! at CurrentJava.TraceThreadPoolExecutor.clientTrace(TraceThreadPoolExecutor.java:22) at CurrentJava.TraceThreadPoolExecutor.execute(TraceThreadPoolExecutor.java:13) at CurrentJava.DivTask.main(DivTask.java:22) Exception in thread “pool-1-thread-1” java.lang.ArithmeticException: / by zero at CurrentJava.DivTask.run(DivTask.java:14) at CurrentJava.TraceThreadPoolExecutor 1. r u n ( T r a c e T h r e a d P o o l E x e c u t o r . j a v a : 30 ) a t j a v a . u t i l . c o n c u r r e n t . T h r e a d P o o l E x e c u t o r . r u n W o r k e r ( T h r e a d P o o l E x e c u t o r . j a v a : 1142 ) a t j a v a . u t i l . c o n c u r r e n t . T h r e a d P o o l E x e c u t o r 1.run(TraceThreadPoolExecutor.java:30) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142) at java.util.concurrent.ThreadPoolExecutor 1.run(TraceThreadPoolExecutor.java:30)atjava.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)atjava.util.concurrent.ThreadPoolExecutorWorker.run(ThreadPoolExecutor.java:617) at java.lang.Thread.run(Thread.java:745) 100.0 50.0 33.0 25.0
可以看出,熟悉的异常又回来了!现在我们不仅可以得到异常发生的Runnable实现内的信息,我们也知道了这个任务是在哪里提交的。这样丰富的信息,我相信可以帮助我们瞬间定位问题。
参考: 《Java高并发程序设计》 葛一鸣 郭超 编著: