Backend Development 14 min read

Understanding the Difference Between execute() and submit() in Java ThreadPoolExecutor

This article explains how Java's ThreadPoolExecutor handles tasks submitted via execute() and submit(), why execute() prints exceptions directly while submit() defers them until Future.get() is called, and provides detailed source‑code analysis and examples to illustrate the underlying mechanisms.

Selected Java Interview Questions
Selected Java Interview Questions
Selected Java Interview Questions
Understanding the Difference Between execute() and submit() in Java ThreadPoolExecutor

Preface

In project development, using multithreading with Callable tasks submitted to a thread pool prints exceptions only when Future.get() is invoked, which seemed odd until the distinction between submit and execute was discovered.

Verification Code

execute method exception printing

Simple code:

public class ThreadPoolTest {
    public static void main(String[] args) {
        ThreadPoolExecutor executorService = buildThreadPoolTaskExecutor();
        executorService.execute(() -> run("execute method"));
        executorService.submit(() -> run("submit method"));
    }
    private static void run(String name) {
        String printStr = "【thread-name:" + Thread.currentThread().getName() + ", execution:" + name + "】";
        System.out.println(printStr);
        throw new RuntimeException(printStr + ", exception occurred");
    }
    private static ThreadPoolExecutor buildThreadPoolTaskExecutor() {
        return new ThreadPoolExecutor(2, 5, 10, TimeUnit.SECONDS,
                new LinkedBlockingQueue<>(100), new ThreadPoolExecutor.AbortPolicy());
    }
}

Result shows that the task submitted via execute prints the exception immediately, while the submit task does not.

submit method exception printing

Modified main :

public static void main(String[] args) {
    ThreadPoolExecutor executorService = buildThreadPoolTaskExecutor();
    // executorService.execute(() -> run("execute method"));
    Future
future = executorService.submit(() -> run("submit method"));
    try {
        future.get();
    } catch (InterruptedException | ExecutionException e) {
        e.printStackTrace();
    }
}

Now the exception from the submit task is printed when Future.get() is called.

Reason

Difference between execute and submit

execute has no return value, accepts a Runnable , and cannot directly indicate task success or failure.

submit returns a Future , accepts a Callable (or a Runnable wrapped as a FutureTask ), allowing the caller to retrieve the result or exception later.

Source Code Analysis

ThreadPoolExecutor#addWorker

private boolean addWorker(Runnable firstTask, boolean core) {
    return false;
}

ThreadPoolExecutor#execute

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);
}

The execute method directly hands the task to a worker; any exception thrown in task.run() propagates and is printed by the worker.

AbstractExecutorService#submit

public
Future
submit(Callable
task) {
    if (task == null) throw new NullPointerException();
    RunnableFuture
ftask = newTaskFor(task);
    execute(ftask);
    return ftask;
}
protected
RunnableFuture
newTaskFor(Callable
callable) {
    return new FutureTask
(callable);
}

The submit method wraps the Callable into a FutureTask , then calls execute . The FutureTask catches exceptions, stores them, and re‑throws them only when get() is invoked.

FutureTask.run and exception handling

public void run() {
    if (state != NEW ||
        !UNSAFE.compareAndSwapObject(this, runnerOffset, null, Thread.currentThread()))
        return;
    try {
        Callable
c = callable;
        if (c != null && state == NEW) {
            V result;
            boolean ran;
            try {
                result = c.call();
                ran = true;
            } catch (Throwable ex) {
                result = null;
                ran = false;
                setException(ex);
            }
            if (ran) set(result);
        }
    } finally {
        runner = null;
        int s = state;
        if (s >= INTERRUPTING)
            handlePossibleCancellationInterrupt(s);
    }
}

Exceptions are captured by setException and stored; they surface only when Future.get() calls report , which throws an ExecutionException containing the original cause.

Summary and Reflection

Summary

Tasks submitted via execute are run in Worker#run ; any exception is re‑thrown and printed immediately.

Tasks submitted via submit are wrapped in a FutureTask ; the exception is stored and only re‑thrown when Future.get() is called.

Thoughts

Using submit is appropriate when the caller needs the result or wants to handle possible failures later, while execute is suitable for fire‑and‑forget Runnable tasks where immediate exception visibility is desired.

JavaconcurrencyThreadPoolExecutorFutureTaskexecutesubmit
Selected Java Interview Questions
Written by

Selected Java Interview Questions

A professional Java tech channel sharing common knowledge to help developers fill gaps. Follow us!

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.