IT박스

UncaughtExceptionHandler가 ExecutorService에서 호출되지 않는 이유는 무엇입니까?

itboxs 2021. 1. 11. 07:54
반응형

UncaughtExceptionHandler가 ExecutorService에서 호출되지 않는 이유는 무엇입니까?


다음과 같이 요약 할 수있는 문제를 발견했습니다.

스레드를 수동으로 만들면 (즉, 인스턴스화 java.lang.Thread하여 UncaughtExceptionHandler) 적절하게 호출됩니다. 그러나 ExecutorServicewith를 사용 ThreadFactory하면 핸들러가 생략됩니다. 내가 놓친 게 무엇입니까?

public class ThreadStudy {

private static final int THREAD_POOL_SIZE = 1;

public static void main(String[] 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<Integer> callable = new Callable<Integer>() {

        @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.");
}
}

예상 : "Uncaught exception ..."메시지가 세 번 표시됩니다.

나는 : 메시지 한 번 (수동으로 생성 된 스레드에 의해 트리거 됨).

Windows 7 및 Mac OS X 10.5에서 Java 1.6으로 재현되었습니다.


예외가 잡히지 않기 때문입니다.

ThreadFactory가 생성하는 Thread에는 Runnable 또는 Callable이 직접 제공되지 않습니다. 대신 사용자가 얻는 Runnable은 내부 작업자 클래스입니다 (예 : ThreadPoolExecutor $ Worker 참조). System.out.println()예제에서 newThread에 주어진 Runnable을 사용해보십시오 .

이 작업자는 제출 된 작업에서 모든 RuntimeException을 포착합니다.

ThreadPoolExecutor # afterExecute 메서드 에서 예외를 가져올 수 있습니다 .


ExecutorService#submit래핑 하기 위해 제출 된 작업에서 throw되고 메서드에 ExcecutionException의해 다시 throw되는 예외입니다 Future.get(). 이는 실행자가 예외를 작업 결과의 일부로 간주하기 때문입니다.

그러나 인터페이스 execute()에서 시작된 메서드를 통해 작업을 제출 Executor하면 UncaughtExceptionHandler에 알림이 전송됩니다.


Java Concurrency in Practice (페이지 163) 책의 인용문 이 도움이되기를 바랍니다.

다소 혼란스럽게도 작업에서 throw 된 예외는 execute와 함께 제출 된 작업에 대해서만 포착되지 않은 예외 처리기로 만듭니다. 제출과 함께 제출 된 작업의 경우 확인 여부에 관계없이 throw 된 예외는 작업의 반환 상태의 일부로 간주됩니다. 제출과 함께 제출 된 작업이 예외와 함께 종료되면 Future.get에 의해 다시 throw되고 ExecutionException에 래핑됩니다.

다음은 그 예입니다.

public class Main {

public static void main(String[] 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();
    }

}

실행을 사용하여 작업을 제출하고 콘솔 출력 "예외 처리기"에서


방금 이전 질문을 살펴본 후 누군가에게 도움이되는 경우 (또는 버그를 놓친 경우) 구현 한 솔루션을 공유 할 수 있다고 생각했습니다.

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.
     * <p>
     * <i>NB:</i> We need this since {@link ExecutorService}s ignore the
     * {@link UncaughtExceptionHandler} of the {@link ThreadFactory}.
     * 
     * @param <V> The result type returned by this FutureTask's get method.
     */
    private class ExceptionHandlingFutureTask<V> extends FutureTask<V> implements RunnableScheduledFuture<V> {

        /** Encapsulated Task */
        private final RunnableScheduledFuture<V> task;

        /**
         * Encapsulate a {@link Callable}.
         * 
         * @param callable
         * @param task
         */
        public ExceptionHandlingFutureTask(Callable<V> callable, RunnableScheduledFuture<V> task) {
            super(callable);
            this.task = task;
        }

        /**
         * Encapsulate a {@link Runnable}.
         * 
         * @param runnable
         * @param result
         * @param task
         */
        public ExceptionHandlingFutureTask(Runnable runnable, RunnableScheduledFuture<V> 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. <i>NB:</i> The thread
     *        reference will always be <code>null</code>.
     */
    public ExceptionHandlingExecuterService(int corePoolSize, UncaughtExceptionHandler eh) {
        super(corePoolSize);
        this.exceptionHandler = eh;
    }

    @Override
    protected <V> RunnableScheduledFuture<V> decorateTask(Callable<V> callable, RunnableScheduledFuture<V> task) {
        return new ExceptionHandlingFutureTask<V>(callable, task);
    }

    @Override
    protected <V> RunnableScheduledFuture<V> decorateTask(Runnable runnable, RunnableScheduledFuture<V> task) {
        return new ExceptionHandlingFutureTask<V>(runnable, task);
    }
}

In addition to Thilos answer: I've written a post about this behavior, if one wants to have it explained a little bit more verbose: https://ewirch.github.io/2013/12/a-executor-is-not-a-thread.html.

Here is a excerpts from the article:

A Thread is capable of processing only one Runable in general. When the Thread.run() method exits the Thread dies. The ThreadPoolExecutor implements a trick to make a Thread process multiple Runnables: it uses a own Runnable implementation. The threads are being started with a Runnable implementation which fetches other Runanbles (your Runnables) from the ExecutorService and executes them: ThreadPoolExecutor -> Thread -> Worker -> YourRunnable. When a uncaught exception occurs in your Runnable implementation it ends up in the finally block of Worker.run(). In this finally block the Worker class tells the ThreadPoolExecutor that it “finished” the work. The exception did not yet arrive at the Thread class but ThreadPoolExecutor already registered the worker as idle.

And here’s where the fun begins. The awaitTermination() method will be invoked when all Runnables have been passed to the Executor. This happens very quickly so that probably not one of the Runnables finished their work. A Worker will switch to “idle” if a exception occurs, before the Exception reaches the Thread class. If the situation is similar for the other threads (or if they finished their work), all Workers signal “idle” and awaitTermination() returns. The main thread reaches the code line where it checks the size of the collected exception list. And this may happen before any (or some) of the Threads had the chance to call the UncaughtExceptionHandler. It depends on the order of execution if or how many exceptions will be added to the list of uncaught exceptions, before the main thread reads it.

A very unexpected behavior. But I won’t leave you without a working solution. So let’s make it work.

We are lucky that the ThreadPoolExecutor class was designed for extensibility. There is a empty protected method afterExecute(Runnable r, Throwable t). This will be invoked directly after the run() method of our Runnable before the worker signals that it finished the work. The correct solution is to extend the ThreadPoolExecutor to handle uncaught exceptions:

 public class ExceptionAwareThreadPoolExecutor extends ThreadPoolExecutor {
     private final List<Throwable> uncaughtExceptions = 
                     Collections.synchronizedList(new LinkedList<Throwable>());

     @Override
     protected void afterExecute(final Runnable r, final Throwable t) {
         if (t != null) uncaughtExceptions.add(t);
     }

     public List<Throwable> getUncaughtExceptions() {
         return Collections.unmodifiableList(uncaughtExceptions);
     }
 }

There is a little bit of a workaround. In your run method, you can catch every exception, and later on do something like this (ex: in a finally block)

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

This will "ensure a firing" of the current exception as thrown to your uncoughtExceptionHandler (or to the defualt uncought exception handler). You can always rethrow catched exceptions for pool worker.

ReferenceURL : https://stackoverflow.com/questions/1838923/why-is-uncaughtexceptionhandler-not-called-by-executorservice

반응형