線程池的堆棧問題
前面的文章已經講了線程池和線程池的內部實現,這篇文章來了解線程池出錯的堆棧信息的打印,畢竟異常堆棧信息的重要性對於程序員來說就像是指南針對於茫茫大海上的船只一樣,沒有指南針船只只能更加艱難的尋找方向,沒有異常堆棧信息,排查問題時,也就只能像大海撈針一樣,慢慢琢磨了。
看下面的例子:
1 public class DivTask implements Runnable { 2 3 int a,b; 4 public DivTask(int a,int b){ 5 this.a = a; 6 this.b = b; 7 }8 @Override 9 public void run() { 10 double re = a / b; 11 System.out.println(re); 12 } 13 //測試 14 public static void main(String[] args){ 15 ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(0,Integer.MAX_VALUE,0L, TimeUnit.SECONDS,new SynchronousQueue<Runnable>());16 for (int i = 0;i < 5;i++){ 17 poolExecutor.submit(new DivTask(100,i)); 18 } 19 } 20 }
上述代碼是將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$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$Worker.run(ThreadPoolExecutor.java:617) at java.lang.Thread.run(Thread.java:745)
可以看出得到的堆棧信息與上面使用execute()方法幾乎一致,都只能知道異常在哪裏拋出的。
3 擴展 ThreadPoolExecutor
我們擴展ThreadPoolExecutor 線程池,讓它在調度任務之前,先保存一下提交任務線程的堆棧信息,如下所示:
1 public class TraceThreadPoolExecutor extends ThreadPoolExecutor { 2 3 public TraceThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) { 4 super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue); 5 } 6 7 @Override 8 public void execute(Runnable task) { 9 super.execute(wrap(task,clientTrace(),Thread.currentThread().getName())); 10 } 11 12 @Override 13 public Future<?> submit(Runnable task) { 14 return super.submit(wrap(task,clientTrace(),Thread.currentThread().getName())); 15 } 16 17 private Exception clientTrace(){ 18 return new Exception("Client stack trace!"); 19 } 20 21 private Runnable wrap(final Runnable task,final Exception clientStack,String clientThreadName){ 22 return new Runnable() { 23 @Override 24 public void run() { 25 try { 26 task.run(); 27 }catch (Exception e){ 28 clientStack.printStackTrace(); 29 throw e; 30 } 31 } 32 }; 33 } 34 }
上述代碼第21行,wrap()方法的第二個參數為一個異常,裏面保存著提交任務的線程堆棧信息。該方法將我們傳入的Runnable對象進行一層包裝,使之能處理異常信息,當任務發生異常時,這個異常就會被打印(第28行)。
將第一個例子的main方法修改為:
1 //測試 2 public static void main(String[] args){ 3 ThreadPoolExecutor poolExecutor = new TraceThreadPoolExecutor(0,Integer.MAX_VALUE,0L,TimeUnit.SECONDS,new SynchronousQueue<Runnable>()); 4 5 for (int i =0;i < 5;i++){ 6 poolExecutor.execute(new DivTask(100,i)); 7 } 8 }
執行,就可以得到下面的信息:
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.run(TraceThreadPoolExecutor.java:30) 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) 100.0 50.0 33.0 25.0
可以看出,熟悉的異常又回來了!現在我們不僅可以得到異常發生的Runnable實現內的信息,我們也知道了這個任務是在哪裏提交的。這樣豐富的信息,我相信可以幫助我們瞬間定位問題。
參考: 《Java高並發程序設計》 葛一鳴 郭超 編著:
線程池的堆棧問題