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.
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.
Selected Java Interview Questions
A professional Java tech channel sharing common knowledge to help developers fill gaps. Follow us!
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.