覆盖 ThreadPoolExecutor 的 afterExecute 方法 - 有什么缺点吗?

9

钩子方法的优点:

beforeExecute(Thread, Runnable)afterExecute(Runnable, Throwable)

beforeExecute(Thread, Runnable)afterExecute(Runnable, Throwable)方法会在每个任务执行前后被调用。这些方法可以用于操作执行环境,例如重新初始化ThreadLocals、收集统计数据或添加日志条目。

我正在使用自定义ThreadPoolExecutor来处理未捕获的异常。我可以在RunnableCallable中添加try{} catch{}块,但是假设遇到无法强制开发人员在相关的RunnableCallable任务中添加这些块的情况。

这个CustomThreadPoolExecutor覆盖了ThreadPoolExecutor中的afterExecute()方法如下(我已将变量b的值分配为零以模拟算术异常):

import java.util.concurrent.*;
import java.util.*;

class CustomThreadPoolExecutor extends ThreadPoolExecutor {

   public CustomThreadPoolExecutor() { 
       super(1,10,60,TimeUnit.SECONDS,new ArrayBlockingQueue<Runnable>(1000));
   }

   protected void afterExecute(Runnable r, Throwable t) {
     super.afterExecute(r, t);
     if (t == null && r instanceof Future<?>) {
       try {
         Object result = ((Future<?>) r).get();
         System.out.println(result);
       } catch (CancellationException ce) {
           t = ce;
       } catch (ExecutionException ee) {
           t = ee.getCause();
       } catch (InterruptedException ie) {
           Thread.currentThread().interrupt(); // ignore/reset
       }
     }
     if (t != null)
       t.printStackTrace();
   }
 }


public class CustomThreadPoolExecutorDemo{

    public static void main(String args[]){
        System.out.println("creating service");
        //ExecutorService service = Executors.newFixedThreadPool(10);
        CustomThreadPoolExecutor service = new CustomThreadPoolExecutor();
        service.submit(new Runnable(){
                 public void run(){
                    int a=4, b = 0;
                    System.out.println("a and b="+a+":"+b);
                    System.out.println("a/b:"+(a/b));
                    System.out.println("Thread Name in Runnable after divide by zero:"+Thread.currentThread().getName());
                 }
            });
        service.shutdown();
    }
}

由于submit()在框架中隐藏了异常,因此我重写了afterExecute()方法来捕获异常。

在这个方法中,我添加了以下语句的阻塞调用

 Object result = ((Future<?>) r).get();

目前我有10个线程,队列容量为1000。假设我的Runnable需要5秒钟才能完成。

通过重写afterExecute()方法,这种方法是否会增加任何性能开销或者存在任何缺点?


2
猜测性能并不是一个好习惯,最好的方法是使用基准测试来比较有和没有覆盖重写的代码,检查是否存在相关变化。https://dev59.com/hHRB5IYBdhLWcg3wz6UK - Jack
2个回答

3
不需要担心你的阻塞调用会增加额外负担,因为任务已经完成执行,并且像在void runWorker(Worker w)中看到的那样具有status >= NORMAL
beforeExecute(wt, task);
Throwable thrown = null;
try {
    task.run();
} catch (RuntimeException x) {
    thrown = x; throw x;
} catch (Error x) {
    thrown = x; throw x;
} catch (Throwable x) {
    thrown = x; throw new Error(x);
} finally {
    afterExecute(task, thrown);
}

但有时候,当我遇到服务间通信问题(例如service.submit()和call future.get()),我的get()会阻塞。 - Ravindra babu
是的,get()方法可能会阻塞,但是afterExecute(Runnable r, Throwable t)方法是受保护的,并且(如果您没有更改FutureTask语义)仅在任务完成后才会在ThreadPoolExecutor中调用。 - dezhik

1
更好的解决方案是,保存从submit()返回的Future,然后您可以在主线程中处理异常,而不是通过修改执行器来打印它。
另一种选择是使用一个实现所需异常处理的公共基本Runnable,例如:
public abstract class BaseRunnable implements Runnable {
  public final run() {
    try {
      runImpl();
    } catch(Throwable t) {
      t.printStackTrace();
    }
  }
  protected abstract runImpl() throws Exception;
}

这在捕获异常并使用setException(ex)FutureTask中不起作用,也不适用于CancellationException。异常将始终被捕获。 - dezhik
@dezthink:如果在主线程中添加以下代码,甚至Callable也可以工作(在重写afterExecute方法之前,我已经这样做过):将future.get()放置在try{} catch{}块中。 - Ravindra babu
@ravindra 我的“不起作用”是关于@jtahlborn建议使用ThreadFactoryUncaughtExceptionHandler,但他在编辑时删除了这些内容 :) - dezhik
@ravindra - 添加了一种替代方案,以简单的方式为您的Runnables添加异常处理。 - jtahlborn
1
@dezhik - 正确,因为您的任务只有在检测到线程中断时才会真正取消,而且只有某些操作会自动执行此操作(例如waitsleep)。实际上,ThreadPoolExecutor的默认取消语义有点误导人,因为它们会告诉您任务已被取消,即使任务仍然快乐地执行,对取消毫不知情。 - jtahlborn
显示剩余6条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接