Executor Framework in Java: Thread Pools, Futures and Runnable Examples

Separate tasks from threads, trace a two-worker pool, and learn how futures, shutdown, cancellation and bounded queues behave in Java.

KnowledgeGate Team

Exam prep & CS education

Updated 22 Sep 20266 min read

Creating new Thread(...) for every small job mixes work with thread creation, makes results awkward, and offers no central control for queueing or shutdown. The Executor Framework separates a submitted task from the policy deciding when and where it runs. Use Runnable or Callable to express work, ExecutorService to control workers and queues, and Future to observe completion or retrieve a result. The Coding & DSA Courses for Placements provide the broader Java and data-structure path.

1. Executor Framework in Java: task, executor, worker and result

Think in four parts: a task is a Runnable or Callable<V>; an executor accepts it; a pool owns reusable workers; a Future<V> represents a computation that may be unfinished. These contracts belong to the Oracle Java SE 25 java.util.concurrent API.

java
Runnable show = () -> System.out.println("ready");
Callable<Integer> doubleSix = () -> 6 * 2;

show returns nothing, and run() cannot declare a checked exception. doubleSix.call() returns 12 and may throw. Executor.execute(show) accepts the first; ExecutorService.submit(doubleSix) returns a Future<Integer>.

The executor controls admission, queueing and its worker set. The operating system still schedules those worker threads on processors. Threads & Process Creation MCQs covers the thread and process background underneath, if you want that first.

2. A complete fixed-thread-pool example with four exact results

java
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class ExecutorTriangularDemo {
    public static void main(String[] args) throws Exception {
        int[] values = {12, 8, 15, 5};
        ExecutorService pool = Executors.newFixedThreadPool(2);
        CountDownLatch startGate = new CountDownLatch(1);
        List<Future<Integer>> futures = new ArrayList<>();

        try {
            for (int value : values) {
                Callable<Integer> triangularTask = () -> {
                    startGate.await();
                    return value * (value + 1) / 2;
                };
                futures.add(pool.submit(triangularTask));
            }

            startGate.countDown();

            List<Integer> triangulars = new ArrayList<>();
            int total = 0;
            for (Future<Integer> future : futures) {
                int triangular = future.get();
                triangulars.add(triangular);
                total += triangular;
            }

            System.out.println("Triangulars: " + triangulars);
            System.out.println("Total: " + total);
        } finally {
            startGate.countDown();
            pool.shutdown();
        }
    }
}

Before release, T1 captures 12 and T2 captures 8; they occupy both workers at startGate.await(). T3 captures 15 and T4 captures 5; they wait in the queue. The finally call is harmless at count zero and releases workers if setup fails before normal release.

Each task computes n * (n + 1) / 2. The results are 12 * 13 / 2 = 78, 8 * 9 / 2 = 36, 15 * 16 / 2 = 120, and 5 * 6 / 2 = 15. Therefore 78 + 36 + 120 + 15 = 249.

Code
Triangulars: [78, 36, 120, 15]
Total: 249

Workers may finish differently, but ordered get() calls keep output in input order.

3. execute, submit and Future: choose by the result you need

Call

Return

Use

execute(Runnable)

Nothing

No caller-side handle is needed

submit(Runnable)

Future<?>

Track completion; successful get() returns null

submit(Callable<V>)

Future<V>

Collect a typed result

For Future<Integer> answer = pool.submit(() -> 21 * 2);, answer.get() yields 42. get() waits if needed; get(300, TimeUnit.MILLISECONDS) returns within the limit or throws TimeoutException. cancel(true) requests interruption, but cannot stop code that ignores it. isDone() and isCancelled() report state, not results or failures.

Future<Integer> failed = pool.submit(() -> 10 / 0); fails with ArithmeticException; failed.get() throws ExecutionException, whose getCause() reveals it. Calling get() after each submission can serialize the workflow, so submit independent tasks before collecting results.

4. Executor shutdown, interruption and timeout are part of correctness

shutdown() rejects new work but lets submitted work finish. shutdownNow() attempts to interrupt active tasks and returns tasks not started. awaitTermination(5, TimeUnit.SECONDS) waits up to 5 seconds after shutdown.

java
pool.shutdown();
try {
    if (!pool.awaitTermination(5, TimeUnit.SECONDS)) {
        pool.shutdownNow();
        pool.awaitTermination(5, TimeUnit.SECONDS);
    }
} catch (InterruptedException e) {
    pool.shutdownNow();
    Thread.currentThread().interrupt();
}

Restoring the flag preserves the cancellation signal for higher-level code. Long tasks must cooperate:

java
for (int chunk = 0; chunk < 1000; chunk++) {
    if (Thread.currentThread().isInterrupted()) return;
    process(chunk);
}

This checks chunks 0 through 999. Catching InterruptedException, clearing it and continuing defeats cancel(true) and shutdownNow(). The triangular-number example finishes before shutdown.

5. Choosing and bounding a thread pool without magic numbers

A single-thread executor runs one task at a time. A fixed-pool convenience factory limits workers but uses an unbounded queue. A cached pool can grow with demand; a scheduled executor handles delayed or periodic work. The example uses 2 only to expose the queue.

With new ThreadPoolExecutor(2, 4, 30, TimeUnit.SECONDS, new ArrayBlockingQueue<>(3), new ThreadPoolExecutor.CallerRunsPolicy()), T1 and T2 take core workers. T3 to T5 fill the queue. T6 and T7 create workers three and four. T8 runs in the submitting thread, slowing submission and applying backpressure.

T1 and T2 fill the two core workers, T3 to T5 the three queue slots, T6 and T7 the extra workers, and T8 runs on the submitting thread.

Choose from the workload: whether tasks are CPU-heavy or wait for I/O, how much concurrency the downstream system can safely absorb, how large the queue may grow, and what latency or rejection policy is acceptable.

6. Executor Framework mistakes: symptom, cause and repair

Mistake

What goes wrong

Repair

New pool per task

Threads and cleanup are wasted

Share a scoped executor

Forget shutdown

Resources remain alive

Close it in finally

Submit, then get()

Concurrency disappears

Submit the batch first

Assume completion order

Output reasoning fails

Choose ordered or completion-oriented handling

Share counter++

Updates can be lost

Use AtomicInteger and wait for every task

Block one worker on nested work

The pool can deadlock

Avoid dependent blocking there

Ignore ExecutionException

Failed work becomes invisible

Inspect and handle the cause

Treat an unbounded queue as free

Memory and waits grow

Bound and monitor overload

For 1,000 increments, a shared int may finish below the logical 1000. AtomicInteger.incrementAndGet() followed by waiting for all tasks yields 1000. In a one-worker pool, an outer task that submits inner work to the same pool and blocks on inner.get() can deadlock because its worker is occupied. The Java Concurrent Collections Tutorial owns atomic map updates, snapshot lists and bounded producer-consumer hand-off; the executor lesson here owns task admission, worker limits, futures and lifecycle control.

7. How Executor Framework questions test understanding

Exercises test Runnable versus Callable, active and queued counts, blocking get(), failure, accidental serialization, shutdown, races and starvation. Mark pool bounds, submissions, queue policy and blocking points, then separate results from execution order.

Attempt each exercise before checking its answer.

  1. Before release, a fixed pool of 2 gets latch-blocked tasks A, B, C, D. How many occupy workers and queue?

  2. Three callables compute triangular numbers for [6, 4, 9]. What do ordered retrieval and the total produce?

  3. With core 2, maximum 4, queue capacity 3, CallerRunsPolicy, and eight non-finishing tasks, where do they go?

Answers: A has 2 worker-occupied and 2 queued. B gives [21, 10, 45] and 21 + 10 + 45 = 76. C has 2 core-active, 3 queued, 2 extra-active, and task eight handled by CallerRunsPolicy.

Output checks: pool.submit(() -> 7 + 5).get() yields 12. pool.submit(() -> { System.out.print("X"); }).get() yields null after completion, but X has no guaranteed timing beside unrelated output without synchronization.

8. Executor Framework in Java: the short version and next step

Keep seven checks: express work with Runnable or Callable; choose execute or submit; set worker and queue policy; submit before blocking; inspect Future failures; protect shared state; define shutdown and overload behaviour. Here, [12, 8, 15, 5] produces triangular numbers [78, 36, 120, 15] and total 249 on two workers.

Use Java Course: Concepts, MCQs and Coding Questions when you want Java concepts and coding practice in sequence. DSA using Java: Placement Preparation Course is a later route for applying Java to data structures and interview problems.

Change the inputs to [6, 10, 3]. Predict triangular numbers [21, 55, 6] and total 82, then run pool sizes 3 and 1. Results stay fixed; maximum simultaneous execution changes from three workers to one.