Pourquoi UncaughtExceptionHandler n’est-il pas appelé par ExecutorService?

Je suis tombé sur un problème, qui peut se résumer comme suit:

Lorsque je crée le thread manuellement (en instanciant java.lang.Thread ), le UncaughtExceptionHandler est appelé de manière appropriée. Cependant, lorsque j’utilise un ExecutorService avec un ThreadFactory le gestionnaire est omis. Qu’est-ce que j’ai raté?

 public class ThreadStudy { private static final int THREAD_POOL_SIZE = 1; public static void main(Ssortingng[] args) { // create uncaught exception handler final UncaughtExceptionHandler exceptionHandler = new UncaughtExceptionHandler() { @Override public void uncaughtException(Thread t, Throwable e) { synchronized (this) { System.err.println("Uncaught exception in thread '" + t.getName() + "': " + e.getMessage()); } } }; // create thread factory ThreadFactory threadFactory = new ThreadFactory() { @Override public Thread newThread(Runnable r) { // System.out.println("creating pooled thread"); final Thread thread = new Thread(r); thread.setUncaughtExceptionHandler(exceptionHandler); return thread; } }; // create Threadpool ExecutorService threadPool = Executors.newFixedThreadPool(THREAD_POOL_SIZE, threadFactory); // create Runnable Runnable runnable = new Runnable() { @Override public void run() { // System.out.println("A runnable runs..."); throw new RuntimeException("Error in Runnable"); } }; // create Callable Callable callable = new Callable() { @Override public Integer call() throws Exception { // System.out.println("A callable runs..."); throw new Exception("Error in Callable"); } }; // a) submitting Runnable to threadpool threadPool.submit(runnable); // b) submit Callable to threadpool threadPool.submit(callable); // c) create a thread for runnable manually final Thread thread_r = new Thread(runnable, "manually-created-thread"); thread_r.setUncaughtExceptionHandler(exceptionHandler); thread_r.start(); threadPool.shutdown(); System.out.println("Done."); } } 

Je m’attends à: trois fois le message “exception non capturée …”

Je reçois: Le message une fois (déclenché par le thread créé manuellement).

Reproduit avec Java 1.6 sous Windows 7 et Mac OS X 10.5.

Parce que l’exception ne va pas sans surprise.

Le Thread que votre ThreadFactory produit ne reçoit pas directement votre Runnable ou Callable. Au lieu de cela, le Runnable que vous obtenez est une classe Worker interne, par exemple, consultez ThreadPoolExecutor $ Worker. Essayez System.out.println() sur le Runnable donné à newThread dans votre exemple.

Ce travailleur intercepte toutes les exceptions d’exécution de votre travail soumis.

Vous pouvez obtenir l’exception dans la méthode ThreadPoolExecutor # afterExecute .

Les exceptions qui sont lancées par des tâches soumises à ExecutorService#submit sont encapsulées dans une ExcecutionException et sont repoussées par la méthode Future.get() . C’est parce que l’exécuteur considère l’exception comme faisant partie du résultat de la tâche.

Si vous soumettez cependant une tâche via la méthode execute() qui provient de l’interface Executor , le UncaughtExceptionHandler est notifié.

Citation du livre Java Concurrency in Practice (page 163), j’espère que cela vous aidera

Assez confus, les exceptions lancées à partir de tâches le rendent uniquement au gestionnaire d’exceptions non capturé pour les tâches soumises avec execute; pour les tâches soumises avec submit, toute exception renvoyée, vérifiée ou non, est considérée comme faisant partie du statut de retour de la tâche. Si une tâche soumise avec submit se termine par une exception, elle est relancée par Future.get, enveloppée dans une exception ExecutionException.

Voici l’exemple:

 public class Main { public static void main(Ssortingng[] args){ ThreadFactory factory = new ThreadFactory(){ @Override public Thread newThread(Runnable r) { // TODO Auto-generated method stub final Thread thread =new Thread(r); thread.setUncaughtExceptionHandler( new Thread.UncaughtExceptionHandler() { @Override public void uncaughtException(Thread t, Throwable e) { // TODO Auto-generated method stub System.out.println("in exception handler"); } }); return thread; } }; ExecutorService pool=Executors.newSingleThreadExecutor(factory); pool.execute(new testTask()); } private static class testTask implements Runnable { @Override public void run() { // TODO Auto-generated method stub throw new RuntimeException(); } } 

J’utilise execute pour soumettre la tâche et les sorties de la console “dans le gestionnaire d’exceptions”

J’ai simplement parcouru mes anciennes questions et j’ai pensé que je pourrais partager la solution que j’ai implémentée au cas où cela aiderait quelqu’un (ou un bogue).

 import java.lang.Thread.UncaughtExceptionHandler; import java.util.concurrent.Callable; import java.util.concurrent.Delayed; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.FutureTask; import java.util.concurrent.RunnableScheduledFuture; import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.ThreadFactory; import java.util.concurrent.TimeUnit; /** * @author Mike Herzog, 2009 */ public class ExceptionHandlingExecuterService extends ScheduledThreadPoolExecutor { /** My ExceptionHandler */ private final UncaughtExceptionHandler exceptionHandler; /** * Encapsulating a task and enable exception handling. * 

* NB: We need this since {@link ExecutorService}s ignore the * {@link UncaughtExceptionHandler} of the {@link ThreadFactory}. * * @param The result type returned by this FutureTask's get method. */ private class ExceptionHandlingFutureTask extends FutureTask implements RunnableScheduledFuture { /** Encapsulated Task */ private final RunnableScheduledFuture task; /** * Encapsulate a {@link Callable}. * * @param callable * @param task */ public ExceptionHandlingFutureTask(Callable callable, RunnableScheduledFuture task) { super(callable); this.task = task; } /** * Encapsulate a {@link Runnable}. * * @param runnable * @param result * @param task */ public ExceptionHandlingFutureTask(Runnable runnable, RunnableScheduledFuture task) { super(runnable, null); this.task = task; } /* * (non-Javadoc) * @see java.util.concurrent.FutureTask#done() The actual exception * handling magic. */ @Override protected void done() { // super.done(); // does nothing try { get(); } catch (ExecutionException e) { if (exceptionHandler != null) { exceptionHandler.uncaughtException(null, e.getCause()); } } catch (Exception e) { // never mind cancelation or interruption... } } @Override public boolean isPeriodic() { return this.task.isPeriodic(); } @Override public long getDelay(TimeUnit unit) { return task.getDelay(unit); } @Override public int compareTo(Delayed other) { return task.compareTo(other); } } /** * @param corePoolSize The number of threads to keep in the pool, even if * they are idle. * @param eh Receiver for unhandled exceptions. NB: The thread * reference will always be null. */ public ExceptionHandlingExecuterService(int corePoolSize, UncaughtExceptionHandler eh) { super(corePoolSize); this.exceptionHandler = eh; } @Override protected RunnableScheduledFuture decorateTask(Callable callable, RunnableScheduledFuture task) { return new ExceptionHandlingFutureTask(callable, task); } @Override protected RunnableScheduledFuture decorateTask(Runnable runnable, RunnableScheduledFuture task) { return new ExceptionHandlingFutureTask(runnable, task); } }

En plus de Thilos, répondez: J’ai écrit un article sur ce comportement, si l’on veut que ce soit expliqué un peu plus en détails: https://ewirch.github.io/2013/12/a-executor-is-not -a-thread.html .

Voici un extrait de l’article:

Un thread est capable de traiter un seul runable en général. Lorsque la méthode Thread.run () quitte le thread, elle meurt. ThreadPoolExecutor implémente une astuce pour rendre un processus Thread plusieurs Runnables: il utilise sa propre implémentation Runnable. Les threads sont démarrés avec une implémentation Runnable qui récupère les autres Runanbles (vos Runnables) à partir de l’ExecutorService et les exécute: ThreadPoolExecutor -> Thread -> Worker -> YourRunnable. Lorsqu’une exception non capturée se produit dans votre implémentation Runnable, elle se retrouve dans le bloc finally de Worker.run (). Dans ce dernier bloc, la classe Worker indique au ThreadPoolExecutor qu’il a «fini» le travail. L’exception n’est pas encore arrivée à la classe Thread mais ThreadPoolExecutor a déjà enregistré le travailleur en tant qu’inactif.

Et voici où commence le plaisir. La méthode waitTermination () sera invoquée lorsque tous les Runnables auront été transmis à l’Executor. Cela se produit très rapidement, de sorte que probablement aucun des Runnables n’a terminé son travail. Un travailleur passera à «inactif» si une exception se produit, avant que l’exception n’atteigne la classe Thread. Si la situation est similaire pour les autres threads (ou s’ils ont terminé leur travail), tous les travailleurs signalent «inactif» et waitTermination () renvoie. Le thread principal atteint la ligne de code où il vérifie la taille de la liste des exceptions collectées. Et cela peut arriver avant que certains (ou certains) des threads aient la chance d’appeler le gestionnaire UncaughtExceptionHandler. Cela dépend de l’ordre d’exécution si et combien d’exceptions seront ajoutées à la liste des exceptions non capturées avant que le thread principal ne le lise.

Un comportement très inattendu. Mais je ne vous laisserai pas sans solution de travail. Alors, faisons que ça marche.

Nous avons de la chance que la classe ThreadPoolExecutor ait été conçue pour l’extensibilité. Il existe une méthode protégée vide afterExecute (Runnable r, Throwable t). Ceci sera invoqué directement après la méthode run () de notre Runnable avant que le travailleur ne signale qu’il a terminé le travail. La solution correcte consiste à étendre le ThreadPoolExecutor pour gérer les exceptions non interceptées:

  public class ExceptionAwareThreadPoolExecutor extends ThreadPoolExecutor { private final List uncaughtExceptions = Collections.synchronizedList(new LinkedList()); @Override protected void afterExecute(final Runnable r, final Throwable t) { if (t != null) uncaughtExceptions.add(t); } public List getUncaughtExceptions() { return Collections.unmodifiableList(uncaughtExceptions); } } 

Il y a un peu de solution. Dans votre méthode run , vous pouvez intercepter toutes les exceptions, puis faire quelque chose comme ça (ex: dans un bloc finally )

 Thread.getDefaultUncaughtExceptionHandler().uncaughtException(Thread.currentThread(), ex); //or, same effect: Thread.currentThread().getUncaughtExceptionHandler().uncaughtException(Thread.currentThread(), ex); 

Cela “assurera un déclenchement” de l’exception en cours telle que lancée sur votre uncoughtExceptionHandler (ou sur le gestionnaire d’exceptions defought). Vous pouvez toujours relancer les exceptions capturées pour le travailleur de pool.