Definition
- Executor – a single-method interface with
execute(Runnable)that runs a task with no return value and no lifecycle control. - ExecutorService – extends Executor and adds
submit(Callablereturning) Future, lifecycle methodsshutdown()andshutdownNow(), and batch operationsinvokeAll()andinvokeAny(). - Runnable – a functional interface with
void run()that cannot return a value or throw checked exceptions. - Callable
– a functional interface with V call() throws Exceptionthat returns a result and can throw checked exceptions. - Future
– a handle to a pending asynchronous result with get()to block until done,isDone()to poll, andcancel()to attempt cancellation. - execute() vs submit() –
execute()is fire-and-forget for Runnable only, whilesubmit()returns a Future for either Runnable or Callable.
Think of the National Bank of India service desk. The Executor is a basic drop-box: Rajesh drops a task form and walks away with no receipt. The ExecutorService is a full service counter: Rajesh drops a form and gets a receipt number (Future), comes back later to collect the result, and the counter has opening and closing hours (shutdown). Runnable is a form that says "process this, no response needed." Callable is a form that says "calculate Rajesh's portfolio value and give me the number." The receipt (Future) tells Rajesh when the result is ready or if something went wrong.
Understanding the Concepts
What is the Executor framework?
The Executor framework lives in java.util.concurrent and separates the concern of what a task does from how it is executed. Before this framework, developers created raw Thread objects for every task, leading to resource exhaustion and poor lifecycle management.
The framework defines a hierarchy of interfaces:
Executorhas one method:execute(Runnable).ExecutorServiceextendsExecutorand adds submission, lifecycle, and batch operations.ScheduledExecutorServiceextendsExecutorServiceand adds scheduling capabilities.ThreadPoolExecutoris the primary implementation class.
package com.bank.core;
import java.util.concurrent.*;
import java.math.BigDecimal;
import java.util.List;
public class ExecutorFrameworkDemo {
public static void main(String[] args) throws Exception {
ExecutorService pool = Executors.newFixedThreadPool(4);
// execute(): fire-and-forget, Runnable only, no return
pool.execute(() -> System.out.println("Trade TRD-001 settled"));
// submit(): returns Future, Callable with result
Future<String> future = pool.submit(() -> {
Thread.sleep(100);
return "SETTLED:TRD-001";
});
String result = future.get(); // blocks until done
System.out.println(result); // SETTLED:TRD-001
pool.shutdown();
}
}
javac -d out com/bank/core/ExecutorFrameworkDemo.java
java -cp out com.bank.core.ExecutorFrameworkDemo
Trade TRD-001 settled
SETTLED:TRD-001
How is Executor different from ExecutorService?
Executor has a single method and no lifecycle control. ExecutorService provides the full set of operations needed in production.
| Feature | Executor | ExecutorService |
|---|---|---|
| Methods | execute(Runnable) | submit(), shutdown(), invokeAll(), invokeAny() |
| Return value | void | Future |
| Lifecycle | None | shutdown(), shutdownNow(), awaitTermination() |
| Batch ops | None | invokeAll(), invokeAny() |
| Input types | Runnable only | Runnable and Callable |
package com.bank.service;
import java.util.concurrent.*;
import java.math.BigDecimal;
public class ExecutorComparison {
public static void main(String[] args) throws Exception {
// Executor: fire-and-forget only
Executor executor = Executors.newSingleThreadExecutor();
executor.execute(() -> System.out.println("Notification sent"));
// ExecutorService: full control
ExecutorService service = Executors.newFixedThreadPool(4);
Future<BigDecimal> balance = service.submit(
() -> new BigDecimal("150000.75")
);
System.out.println("Balance: Rs." + balance.get());
service.shutdown();
}
}
Notification sent
Balance: Rs.150000.75
How is Runnable different from Callable?
Runnable and Callable are both task abstractions, but they differ in return value and exception handling.
| Feature | Runnable | Callable<V> |
|---|---|---|
| Method | `void run()` | `V call() throws Exception` |
| Return value | None | Returns V |
| Checked exceptions | Cannot throw | Can throw any Exception |
| Thread constructor | Yes | No |
| Lambda target | `() -> { }` | `() -> { return value; }` |package com.bank.service;
import java.util.concurrent.*;
import java.math.BigDecimal;
public class RunnableVsCallable {
public static void main(String[] args) throws Exception {
ExecutorService pool = Executors.newFixedThreadPool(2);
// Runnable: no return, no checked exception
Runnable notification = () -> {
System.out.println("SMS sent to Rajesh: Trade settled");
};
pool.execute(notification);
// Callable: returns BigDecimal, can throw checked exception
Callable<BigDecimal> navCalculation = () -> {
BigDecimal nav = new BigDecimal("1500000.00");
if (nav.compareTo(BigDecimal.ZERO) <= 0) {
throw new Exception("NAV calculation failed");
}
return nav;
};
Future<BigDecimal> navFuture = pool.submit(navCalculation);
System.out.println("NAV: Rs." + navFuture.get());
pool.shutdown();
}
}
SMS sent to Rajesh: Trade settled
NAV: Rs.1500000.00
How does Future work?
Future is a handle to a pending asynchronous result. It lets you check completion status, retrieve the result, or cancel the task.
package com.bank.service;
import java.util.concurrent.*;
public class FutureDemo {
public static void main(String[] args) throws Exception {
ExecutorService pool = Executors.newFixedThreadPool(2);
Future<String> settlement = pool.submit(() -> {
Thread.sleep(200);
return "SETTLED:TRD-001";
});
System.out.println("Done? " + settlement.isDone()); // false
String result = settlement.get(5, TimeUnit.SECONDS); // blocks with timeout
System.out.println("Done? " + settlement.isDone()); // true
System.out.println("Result: " + result);
System.out.println("Cancelled? " + settlement.isCancelled()); // false
pool.shutdown();
}
}
Done? false
Done? true
Result: SETTLED:TRD-001
Cancelled? false
How is execute() different from submit()?
execute() takes only Runnable and returns void. If the task throws an exception, it goes to the thread's UncaughtExceptionHandler and may be silently lost. submit() takes Runnable or Callable and returns a Future. Exceptions are captured inside the Future and surface only when get() is called.
package com.bank.service;
import java.util.concurrent.*;
public class ExecuteVsSubmit {
public static void main(String[] args) {
ExecutorService pool = Executors.newFixedThreadPool(2);
// execute(): exception goes to UncaughtExceptionHandler
pool.execute(() -> {
throw new RuntimeException("Settlement crashed!");
// Exception printed to stderr, worker thread dies and is replaced
});
// submit(): exception captured in Future
Future<?> future = pool.submit(() -> {
throw new RuntimeException("Settlement crashed!");
});
try {
future.get(); // ExecutionException wrapping RuntimeException
} catch (ExecutionException e) {
System.out.println("Caught: " + e.getCause().getMessage());
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
pool.shutdown();
}
}
Caught: Settlement crashed!
How do invokeAll() and invokeAny() work?
invokeAll() submits a batch of Callable tasks and blocks until ALL complete, returning a list of Futures. invokeAny() returns the result of the FIRST successful completion and may cancel the remaining tasks.
package com.bank.service;
import java.util.concurrent.*;
import java.util.List;
public class BatchOperations {
public static void main(String[] args) throws Exception {
ExecutorService pool = Executors.newFixedThreadPool(4);
List<Callable<String>> settlements = List.of(
() -> { Thread.sleep(100); return "SETTLED:TRD-001"; },
() -> { Thread.sleep(200); return "SETTLED:TRD-002"; },
() -> { Thread.sleep(50); return "SETTLED:TRD-003"; }
);
// invokeAll: blocks until ALL done
List<Future<String>> allResults = pool.invokeAll(settlements);
for (Future<String> f : allResults) {
System.out.println(f.get());
}
// invokeAny: returns FIRST successful result
String fastest = pool.invokeAny(settlements);
System.out.println("Fastest: " + fastest);
pool.shutdown();
}
}
SETTLED:TRD-001
SETTLED:TRD-002
SETTLED:TRD-003
Fastest: SETTLED:TRD-003
What are common mistakes with the Executor framework?
The most common mistake is forgetting to call shutdown() on the pool, which causes the JVM to hang indefinitely.
// WRONG: JVM never exits
public static void main(String[] args) {
ExecutorService pool = Executors.newFixedThreadPool(4);
pool.submit(() -> System.out.println("Done"));
// main() returns but pool threads are alive: JVM hangs!
}
// CORRECT: always shut down
public static void main(String[] args) throws Exception {
ExecutorService pool = Executors.newFixedThreadPool(4);
try {
pool.submit(() -> System.out.println("Done"));
} finally {
pool.shutdown();
pool.awaitTermination(30, TimeUnit.SECONDS);
}
}
Another mistake is never calling future.get() after submit(), which silently swallows any exceptions thrown by the task.
When should you use the Executor framework?
Use the Executor framework whenever you need concurrent task execution in production. It replaces manual new Thread() patterns with managed thread pools. Use execute() for fire-and-forget tasks like sending notifications. Use submit() when you need the result or need to know if the task succeeded. Use invokeAll() for batch processing like EOD settlement of 500 trades. Use invokeAny() for racing multiple strategies, such as fetching a price from the fastest exchange.
What about thread safety and the Executor framework?
The Executor framework handles thread management, but your tasks must still be thread-safe. Shared mutable state inside tasks requires synchronization. Use BigDecimal for money calculations to avoid floating-point issues. Use ConcurrentHashMap for shared maps. Use AtomicLong for shared counters.
What are common interview follow-up questions?
- What is the difference between
shutdown()andshutdownNow()? - How does
Future.cancel(true)interact withInterruptedException? - Why does
invokeAny()cancel remaining tasks after the first success? - What happens if you submit a task to a pool that has already been shut down?
- How does
ThreadPoolExecutordecide when to create new threads versus queueing tasks?
Visual Overview
┌──────────────────────────────────────────────────┐
│ Executor Framework Hierarchy │
│ │
│ Executor │
│ │ execute(Runnable) : fire-and-forget │
│ │ │
│ └─ ExecutorService │
│ │ submit(Callable) : returns Future<V> │
│ │ shutdown() / shutdownNow() │
│ │ invokeAll() / invokeAny() │
│ │ │
│ └─ ScheduledExecutorService │
│ │ schedule() / scheduleAtFixedRate() │
│ │ │
│ └─ ThreadPoolExecutor (impl) │
└──────────────────────────────────────────────────┘
Task Submission Flow:
Runnable/Callable
│
▼
┌─────────────┐ ┌──────────────┐ ┌───────────┐
│ submit() │────▶│ Work Queue │────▶│ Worker │
│ execute() │ │ (Blocking) │ │ Threads │
└─────────────┘ └──────────────┘ └───────────┘
│ │
▼ ▼
Future<V> Execute task
(handle to result) Return result
Key Takeaway
The Executor framework is the foundation of modern Java concurrency. It separates what a task does from how it runs. Instead of creating raw Thread objects for every piece of work, you submit tasks to a managed pool that handles thread creation, reuse, and cleanup.
The framework has three layers. Executor provides a single execute(Runnable) method for fire-and-forget work. ExecutorService adds submit() returning a Future, lifecycle management with shutdown(), and batch operations with invokeAll() and invokeAny(). ScheduledExecutorService adds scheduling capabilities on top of that.
The choice between Runnable and Callable is simple. Runnable returns nothing and cannot throw checked exceptions. Callable returns a value and can throw anything. When you submit a Callable, you get a Future that holds the pending result. Calling future.get() blocks until the result is ready or an exception is thrown.
In our banking work at National Bank of India, every concurrent operation goes through the Executor framework. Portfolio NAV calculation for 500 funds uses invokeAll() with 50 threads for a 10x speedup. Price discovery from multiple exchanges uses invokeAny() to return the fastest response.
Remember one sentence: the Executor framework decouples task submission from execution, with Executor for fire-and-forget and ExecutorService for submit-track-collect.
---