Java thread pool "eaten" thread exception (with source code analysis and solution)

Keywords: Java jvm JDK

Preface

Today, I encountered a bug. The phenomenon is that a task is put into the thread pool, which seems to be "not executed" and the log is not typed.

After debugging the local code, it was found that NPE was thrown in the first half of the task logic, but there was no try-catch in the outer part of the code, which led to the exception being eaten.

The solution to this problem is very simple, just add a try-catch to the outer layer, but if this exception is not caught, how does the thread pool internal logic handle this exception? Where does this anomaly end up?

With doubts and curiosity, I studied the source code of the thread pool and made the following summary.

Source code analysis

The code for the problem in the project looks like this

ExecutorService threadPool = Executors.newFixedThreadPool(3);

threadPool.submit(() -> {
    String pennyStr = null;
    Double penny = Double.valueOf(pennyStr);
    ...
})

Advance to the new Fixed ThreadPool factory method to see the specific implementation class generated, and find that it is ThreadPool Executor.

public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }

Looking at the inheritance relationship of this class,

Enter the submit method, which is stipulated in the ExecutorService interface and is actually implemented in AbstractExectorService. ThreadPoolExecutor does not have the override method.

 public Future<?> submit(Runnable task) {
        if (task == null) throw new NullPointerException();
        RunnableFuture<Void> ftask = newTaskFor(task, null);
        execute(ftask);
        return ftask;
    }

protected <T> RunnableFuture<T> newTaskFor(Runnable runnable, T value) {
        return new FutureTask<T>(runnable, value);
    }

Construction Method of Corresponding FutureTask Object

public FutureTask(Runnable runnable, V result) {
        this.callable = Executors.callable(runnable, result);
        this.state = NEW;       // state is decorated with volatile to ensure visibility under multithreading
    }

Construction Method of Corresponding Callable Object

public static <T> Callable<T> callable(Runnable task, T result) {
        if (task == null)
            throw new NullPointerException();
        return new RunnableAdapter<T>(task, result);
    }

Construction Method of Corresponding Runnable Adapter Object

 /**
     * A callable that runs given task and returns given result
     * A Callable object that performs the given task and returns the result
     */
    static final class RunnableAdapter<T> implements Callable<T> {
        final Runnable task;
        final T result;
        RunnableAdapter(Runnable task, T result) {
            this.task = task;
            this.result = result;
        }
        public T call() {
            task.run();
            return result;
        }
    }

To sum up, newTaskFor wraps our submitted Runable object into a Future.

The next step is to submit tasks to a queue for thread pool scheduling.

public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
    
        int c = ctl.get();
        if (workerCountOf(c) < corePoolSize) {
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }
        if (isRunning(c) && workQueue.offer(command)) {
            int recheck = ctl.get();
            if (! isRunning(recheck) && remove(command))
                reject(command);
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false);
        }
        else if (!addWorker(command, false))
            reject(command);
    }

Because the main concern is how the thread executes, throws and handles exceptions, we do not parse the redundant logic for the time being. It's easy to find that if the task is to be executed, it must be in the addWorker method. So let's go in and see. Because the addWorker method is very long and doesn't want to list too much code, I've picked up the key code segments:

private boolean addWorker(Runnable firstTask, boolean core) {

   ...
   boolean workerStarted = false;
   boolean workerAdded = false;
   Worker w = null;
   try {
      // Instantiate a worker object
      w = new Worker(firstTask);
      final Thread t = w.thread;
      if (t != null) {
          final ReentrantLock mainLock = this.mainLock;
          mainLock.lock();
          try {
            
              int rs = runStateOf(ctl.get());

              if (rs < SHUTDOWN ||
                  (rs == SHUTDOWN && firstTask == null)) {
                  if (t.isAlive()) // precheck that t is startable
                      throw new IllegalThreadStateException();
                  workers.add(w);
                  int s = workers.size();
                  if (s > largestPoolSize)
                      largestPoolSize = s;
                  workerAdded = true;
              }
          } finally {
              mainLock.unlock();
          }
          if (workerAdded) {
              // From the construction method of Worker object, when the thread object start s,
              // Then it actually calls run() of the Worker object.
              t.start();
              workerStarted = true;
          }
      }
   } finally {
      if (! workerStarted)
          addWorkerFailed(w);
   }
   return workerStarted;
}

// Construction Method of Worker
  Worker(Runnable firstTask) {
            setState(-1); // inhibit interrupts until runWorker
            this.firstTask = firstTask;
            this.thread = getThreadFactory().newThread(this);
        }
 

Let's look at the inner class Worker object of ThreadPoolExecutor:

private final class Worker
        extends AbstractQueuedSynchronizer
        implements Runnable
   {
        ...

        /** Delegates main run loop to outer runWorker  */
        public void run() {
            runWorker(this);
        }

      ...
   }

It seems that the real task is in this external runWorker. Let's see how this method consumes Worker threads.

final void runWorker(Worker w) {
    Thread wt = Thread.currentThread();
    Runnable task = w.firstTask;
    w.firstTask = null;
    w.unlock(); // allow interrupts
    boolean completedAbruptly = true;
    try {
        while (task != null || (task = getTask()) != null) {
            w.lock();
   
            if ((runStateAtLeast(ctl.get(), STOP) ||
                 (Thread.interrupted() &&
                  runStateAtLeast(ctl.get(), STOP))) &&
                !wt.isInterrupted())
                wt.interrupt();
            try {
                beforeExecute(wt, task);
                Throwable thrown = null;
                // ==== Key code start====
                try {
                    // Very concise, calling the run method of the task
                    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);
                }
                 // ==== Key code end====
            } finally {
                task = null;
                w.completedTasks++;
                w.unlock();
            }
        }
        completedAbruptly = false;
    } finally {
        processWorkerExit(w, completedAbruptly);
    }
}

At last, you can see that the try-catch block code block in the key code calls the run method of this task.

// ==== Key code start====
try {
  // Very concise, calling the run method of the task
  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);
}
// ==== Key code end====

You can see that after the exception is captured, it will be thrown out again, except that there is a afterExecute() method in the last block, which seems to be able to handle the exception information here. Go in and have a look.

protected void afterExecute(Runnable r, Throwable t) { }

You can see that nothing has been done in the ThreadPoolExecutor#afterExecute() method, which seems to allow users to customize the logic after task execution through override, which can include exception handling.

So where did the exception go? I found the logic of hotSpot JVM to handle thread exceptions in a big guy's article.

if (!destroy_vm || JDK_Version::is_jdk12x_version()) {
    // JSR-166: change call from from ThreadGroup.uncaughtException to
    // java.lang.Thread.dispatchUncaughtException
    if (uncaught_exception.not_null()) {
      //If there are uncovered exceptions
      Handle group(this, java_lang_Thread::threadGroup(threadObj()));
      {
        KlassHandle recvrKlass(THREAD, threadObj->klass());
        CallInfo callinfo;
        KlassHandle thread_klass(THREAD, SystemDictionary::Thread_klass());
        /*  
         This is similar to a method table, which actually calls the Thread_ dispatch UncaughtException method.
         template(dispatchUncaughtException_name,            "dispatchUncaughtException")                
        */
        LinkResolver::resolve_virtual_call(callinfo, threadObj, recvrKlass, thread_klass,
                                           vmSymbols::dispatchUncaughtException_name(),
                                           vmSymbols::throwable_void_signature(),
                                           KlassHandle(), false, false, THREAD);
        CLEAR_PENDING_EXCEPTION;
        methodHandle method = callinfo.selected_method();
        if (method.not_null()) {
          JavaValue result(T_VOID);
          JavaCalls::call_virtual(&result,
                                  threadObj, thread_klass,
                                  vmSymbols::dispatchUncaughtException_name(),
                                  vmSymbols::throwable_void_signature(),
                                  uncaught_exception,
                                  THREAD);
        } else {
          KlassHandle thread_group(THREAD, SystemDictionary::ThreadGroup_klass());
          JavaValue result(T_VOID);
          JavaCalls::call_virtual(&result,
                                  group, thread_group,
                                  vmSymbols::uncaughtException_name(),
                                  vmSymbols::thread_throwable_void_signature(),
                                  threadObj,           // Arg 1
                                  uncaught_exception,  // Arg 2
                                  THREAD);
        }
        if (HAS_PENDING_EXCEPTION) {
          ResourceMark rm(this);
          jio_fprintf(defaultStream::error_stream(),
                "\nException: %s thrown from the UncaughtExceptionHandler"
                " in thread \"%s\"\n",
                pending_exception()->klass()->external_name(),
                get_thread_name());
          CLEAR_PENDING_EXCEPTION;
        }
      }
    }

The code is written in C. If you are interested, you can go to the full text. You can read it a little bit according to the English annotations.

http://hg.openjdk.java.net/jd...

As you can see, the Thread dispatchUncaughtException method will eventually be invoked here:

/**
     * Dispatch an uncaught exception to the handler. This method is
     * intended to be called only by the JVM.
     */
    private void dispatchUncaughtException(Throwable e) {
        getUncaughtExceptionHandler().uncaughtException(this, e);
    }
/**
 * Called by the Java Virtual Machine when a thread in this
 * thread group stops because of an uncaught exception, and the thread
 * does not have a specific {@link Thread.UncaughtExceptionHandler}
 * installed.
 *
 */
public void uncaughtException(Thread t, Throwable e) {
        if (parent != null) {
            parent.uncaughtException(t, e);
        } else {
            Thread.UncaughtExceptionHandler ueh =
                Thread.getDefaultUncaughtExceptionHandler();
            if (ueh != null) {
                ueh.uncaughtException(t, e);
            } else if (!(e instanceof ThreadDeath)) {
               //You can see that it's going to hit System.err.
                System.err.print("Exception in thread \""
                                 + t.getName() + "\" ");
                e.printStackTrace(System.err);
            }
        }
    }

The jdk annotation also makes it clear that when a thread throws an uncovered exception, the JVM calls the method. If the current thread does not declare the UncaughtException Handler member variable and override the uncaughtException method, it will see if the thread group to which the thread belongs (if there is a thread group) has this class, and if not, it will be typed into System.err.

IBM's article also encourages us to use the uncaughtException handler provided by ThreadGroup to detect abnormal termination of threads.

https://www.ibm.com/developer...

Summary (Solutions)

From the above source code analysis, we can see that there are several methods for the abnormal "eaten" problem in this article.

  1. Capture with try-catch, usually with this
  2. Threads or thread group objects set UncaughtExceptionHandler member variables

      Thread t = new Thread(r);
                t.setUncaughtExceptionHandler(
                    (t1, e) -> LOGGER.error(t1 + " throws exception: " + e));
                return t;
  3. The afterExecute method of the override thread pool.

Although this article is a solution to the problem, but the main idea is to analyze the source code, understand the process of the whole process of exceptions, hoping to help you.

Reference resources

  1. https://www.jcp.org/en/jsr/de...
  2. https://www.ibm.com/developer...
  3. http://ifeve.com/%E6%B7%B1%E5...

Posted by fresch on Wed, 24 Apr 2019 09:30:35 -0700