Back to Thread Pool & Executor Framework
Q2Question 2 of 8

What is the Executor framework?

Key Concepts
  • The Executor framework decouples task submission from task execution via java.util.concurrent interfaces.
  • Executor has one method execute(Runnable) for fire-and-forget tasks with no return value.
  • ExecutorService extends Executor with submit(), shutdown(), invokeAll(), and invokeAny() for full lifecycle management.
  • Runnable.run() returns void and cannot throw checked exceptions, while Callable.call() returns a value and can throw checked exceptions.
  • Future is a handle to a pending result: get() blocks, isDone() polls, cancel() attempts cancellation.
  • execute() is fire-and-forget (Runnable only), submit() returns a Future (Runnable or Callable).
  • Exceptions thrown in execute() go to UncaughtExceptionHandler, while submit() captures them inside the Future.

Definition

The Executor framework decouples task submission from task execution: Executor provides fire-and-forget execute(Runnable), ExecutorService adds submit(Callable) returning Future for result retrieval, lifecycle management, and batch operations.
  • 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(Callable) returning Future, lifecycle methods shutdown() and shutdownNow(), and batch operations invokeAll() and invokeAny().
  • Runnable – a functional interface with void run() that cannot return a value or throw checked exceptions.
  • Callable – a functional interface with V call() throws Exception that 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, and cancel() to attempt cancellation.
  • execute() vs submit() – execute() is fire-and-forget for Runnable only, while submit() 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:

  • Executor has one method: execute(Runnable).
  • ExecutorService extends Executor and adds submission, lifecycle, and batch operations.
  • ScheduledExecutorService extends ExecutorService and adds scheduling capabilities.
  • ThreadPoolExecutor is the primary implementation class.
JAVA
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();
    }
}
BASH
javac -d out com/bank/core/ExecutorFrameworkDemo.java
java -cp out com.bank.core.ExecutorFrameworkDemo
TEXT
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 |

JAVA
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();
    }
}
TEXT
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.

DIAGRAM
| 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; }` |
JAVA
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();
    }
}
TEXT
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.

JAVA
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();
    }
}
TEXT
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.

JAVA
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();
    }
}
TEXT
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.

JAVA
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();
    }
}
TEXT
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.

JAVA
// 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() and shutdownNow()?
  • How does Future.cancel(true) interact with InterruptedException?
  • 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 ThreadPoolExecutor decide when to create new threads versus queueing tasks?

Visual Overview

DIAGRAM
  ┌──────────────────────────────────────────────────┐
  │              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.

---

Share
Use ← → arrow keys to navigate