CodeToConcept
About

java

java Tutorials

Introduction
Java Variables
Java Data Types
Java Type Casting
Java Operators
Java Input/Output
Java Class & Object
java Method
Java Constructor
Java this keyword
Java super keyword
Java Method Overloading and Overriding
Java Interface
Java Abstract Class
Java Memory Management
Design Patterns
JDBC (Database Connectivity)
Java IDEs

java Tutorials

Introduction
Java Variables
Java Data Types
Java Type Casting
Java Operators
Java Input/Output
Java Class & Object
java Method
Java Constructor
Java this keyword
Java super keyword
Java Method Overloading and Overriding
Java Interface
Java Abstract Class
Java Memory Management
Design Patterns
JDBC (Database Connectivity)
Java IDEs

Sample Question

Think it through, then check the tutorial.

CodeToConcept

Quickly learn through simple, structured, and practical coding tutorials. Build your confidence from step zero to real-world experience.

© 2026 CodeToConcept. All rights reserved.

Quick Links

AboutTutorials

Tutorials

JavaHTMLCSS

Support

Privacy PolicyTerms of ServiceContact
GitHubTwitterYouTube

Java Executor Framework

An executor accepts tasks so application code does not need to manage a new thread for every job. ExecutorService adds task results and lifecycle operations. Choosing its queue, concurrency limit, and shutdown behavior is part of using it correctly.

Why Do We Need the Executor Framework?

When an application has many tasks to execute, creating a new thread for every task can become difficult to manage.

For example:

for (int i = 1; i <= 100; i++) {
    Thread thread = new Thread(() -> {
        // perform task
    });
 
    thread.start();
}

This approach creates a large number of threads.

Creating too many threads can:

  • Consume more memory
  • Increase thread-management overhead
  • Reduce application performance
  • Make thread lifecycle management difficult
  • Make it harder to control how many tasks execute simultaneously

The Executor Framework solves this problem by separating task submission from thread management.

We submit tasks, and the executor decides which available thread should execute them.

Executor Framework Architecture

The basic idea is:

            Tasks
               |
               v
       +----------------+
       |    Executor    |
       +----------------+
               |
               v
        +--------------+
        | Thread Pool  |
        +--------------+
         /      |      \
        v       v       v
     Thread  Thread  Thread

The application submits tasks to the executor.

The executor manages a pool of threads and assigns tasks to available threads.

Important Interfaces and Classes

The Executor Framework mainly uses:

TypePurpose
ExecutorExecutes submitted tasks
ExecutorServiceProvides lifecycle management and task submission features
ScheduledExecutorServiceExecutes tasks after a delay or periodically
ExecutorsFactory class for creating executor services
ThreadPoolExecutorConfigurable thread-pool implementation
ScheduledThreadPoolExecutorThread pool implementation for scheduled tasks
FutureRepresents the result of an asynchronous computation
CallableRepresents a task that can return a result

1. Executor Interface

Executor is the basic interface of the Executor Framework.

It provides one main method:

void execute(Runnable command);

Example

import java.util.concurrent.Executor;
 
public class HelloWorld {
    public static void main(String[] args) {
 
        Executor executor = command -> {
            new Thread(command).start();
        };
 
        executor.execute(() -> {
            System.out.println("Task is running");
        });
    }
}

Here, we submit a Runnable task using execute().

The Executor interface focuses only on executing tasks. It does not provide methods for shutting down the executor or obtaining task results.

2. ExecutorService

ExecutorService extends the Executor interface and provides additional functionality for managing tasks and the executor lifecycle.

Some commonly used methods are:

  • execute()
  • submit()
  • shutdown()
  • shutdownNow()
  • isShutdown()
  • isTerminated()
  • awaitTermination()
  • invokeAll()
  • invokeAny()

Example

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
 
public class HelloWorld {
    public static void main(String[] args) {
 
        ExecutorService executor = Executors.newFixedThreadPool(2);
 
        executor.execute(() -> {
            System.out.println("Task 1");
        });
 
        executor.execute(() -> {
            System.out.println("Task 2");
        });
 
        executor.shutdown();
    }
}

Here, Executors.newFixedThreadPool(2) creates an executor with a pool containing up to two worker threads.

ExecutorService as a Thread Pool

A thread pool is a collection of reusable worker threads.

For example:

                ExecutorService
                       |
                 Thread Pool
                /     |     \
               /      |      \
          Thread 1  Thread 2  Thread 3

Suppose there are five tasks and only two worker threads:

Tasks:
T1 T2 T3 T4 T5
 
Thread Pool:
Thread 1
Thread 2

Initially:

Thread 1 -> T1
Thread 2 -> T2

When a thread finishes:

Thread 1 -> T3
Thread 2 -> T4

Then:

Thread 1 -> T5
Thread 2 -> available

The threads can be reused instead of creating a new thread for every task.

Creating a Fixed Thread Pool

A fixed-size thread pool can be created using:

ExecutorService executor =
        Executors.newFixedThreadPool(3);

This creates an executor that uses up to three worker threads.

Example

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
 
public class HelloWorld {
    public static void main(String[] args) {
 
        ExecutorService executor =
                Executors.newFixedThreadPool(3);
 
        for (int i = 1; i <= 5; i++) {
 
            int taskNumber = i;
 
            executor.execute(() -> {
                System.out.println(
                    "Executing Task " + taskNumber +
                    " by " + Thread.currentThread().getName()
                );
            });
        }
 
        executor.shutdown();
    }
}

The output order is not guaranteed because multiple tasks may execute concurrently.

execute() Method

The execute() method accepts a Runnable.

executor.execute(task);

Example

executor.execute(() -> {
    System.out.println("Task executed");
});

It is useful when we only need to execute a task and do not need a result.

submit() Method

submit() is more flexible than execute().

It can accept:

  • Runnable
  • Callable

and returns a Future.

Example with Runnable:

ExecutorService executor =
        Executors.newFixedThreadPool(2);
 
Future<?> future = executor.submit(() -> {
    System.out.println("Task executed");
});
 
executor.shutdown();

The returned Future can be used to track the submitted task.

Callable

Callable is similar to Runnable, but it can:

  • Return a result
  • Throw checked exceptions

Example

Callable<Integer> task = () -> {
    return 10 + 20;
};

We can submit it to an executor:

ExecutorService executor =
        Executors.newFixedThreadPool(2);
 
Future<Integer> future = executor.submit(task);

We can obtain the result using:

Integer result = future.get();

Complete example:

import java.util.concurrent.*;
 
public class HelloWorld {
    public static void main(String[] args)
            throws Exception {
 
        ExecutorService executor =
                Executors.newFixedThreadPool(2);
 
        Callable<Integer> task = () -> {
            return 10 + 20;
        };
 
        Future<Integer> future =
                executor.submit(task);
 
        Integer result = future.get();
 
        System.out.println("Result: " + result);
 
        executor.shutdown();
    }
}

Output

Result: 30

Future

Future represents the result of an asynchronous computation.

When we submit a Callable, we immediately receive a Future.

Future<Integer> future =
        executor.submit(task);

The task may still be running when submit() returns.

We can later retrieve its result:

Integer result = future.get();

Some important methods of Future are:

MethodPurpose
get()Waits for and returns the result
get(timeout, unit)Waits for a limited time
isDone()Checks whether the task has completed
isCancelled()Checks whether the task was cancelled
cancel()Attempts to cancel the task

Future.get() Can Block

The following statement:

future.get();

waits if the task has not finished yet.

Example

Future<Integer> future = executor.submit(() -> {
    Thread.sleep(3000);
    return 100;
});
 
System.out.println(future.get());

The calling thread waits until the task completes and produces the result.

A timeout can be used when we do not want to wait indefinitely:

Integer result =
        future.get(2, TimeUnit.SECONDS);

If the result is not available within the specified time, a TimeoutException can occur.

shutdown()

After submitting all required tasks, the executor should normally be shut down.

executor.shutdown();

shutdown():

  • Stops accepting new tasks
  • Allows already submitted tasks to complete
  • Begins an orderly shutdown

Example

executor.shutdown();

It does not immediately interrupt currently running tasks.

shutdownNow()

shutdownNow() attempts to stop the executor more aggressively.

List<Runnable> tasks =
        executor.shutdownNow();

It:

  • Attempts to interrupt running tasks
  • Prevents queued tasks from starting
  • Returns tasks that were awaiting execution

However, interruption is cooperative. A running task does not necessarily stop immediately just because shutdownNow() was called.

isShutdown()

Checks whether shutdown has been initiated.

executor.isShutdown();

Example

executor.shutdown();
 
System.out.println(executor.isShutdown());

Output

true

isTerminated()

Checks whether all tasks have completed after shutdown.

executor.isTerminated();

A useful distinction is:

shutdown()
    |
    v
No new tasks accepted
    |
    v
Existing tasks complete
    |
    v
Terminated

awaitTermination()

awaitTermination() allows a thread to wait for the executor to terminate.

Example

executor.shutdown();
 
try {
    if (executor.awaitTermination(10, TimeUnit.SECONDS)) {
        System.out.println("Executor terminated");
    }
} catch (InterruptedException e) {
    Thread.currentThread().interrupt();
}

Common Executor Types Created Using Executors

The Executors utility class provides factory methods for creating different executor configurations.

Common examples include:

Executors.newFixedThreadPool(5);
Executors.newSingleThreadExecutor();
Executors.newCachedThreadPool();
Executors.newScheduledThreadPool(3);

Fixed Thread Pool

ExecutorService executor =
        Executors.newFixedThreadPool(4);

It uses a fixed number of worker threads.

Suitable when we want to limit the number of tasks executing concurrently.

Example

ExecutorService executor =
        Executors.newFixedThreadPool(4);
 
for (int i = 1; i <= 10; i++) {
 
    int task = i;
 
    executor.submit(() -> {
        System.out.println("Task " + task);
    });
}
 
executor.shutdown();

At most four worker threads are used by this executor for these tasks.

Single Thread Executor

ExecutorService executor =
        Executors.newSingleThreadExecutor();

It uses one worker thread.

Tasks are executed sequentially by that worker.

Example

ExecutorService executor =
        Executors.newSingleThreadExecutor();
 
executor.execute(() ->
        System.out.println("Task 1"));
 
executor.execute(() ->
        System.out.println("Task 2"));
 
executor.execute(() ->
        System.out.println("Task 3"));
 
executor.shutdown();

The tasks are processed one at a time.

However, if task execution itself has asynchronous behavior or uses other threads, the overall application behavior can be more complex.

Cached Thread Pool

ExecutorService executor =
        Executors.newCachedThreadPool();

A cached thread pool can create new threads when needed and reuse previously created idle threads.

It is designed for workloads with many short-lived asynchronous tasks.

Example

ExecutorService executor =
        Executors.newCachedThreadPool();
 
for (int i = 1; i <= 10; i++) {
 
    int task = i;
 
    executor.execute(() -> {
        System.out.println("Task " + task);
    });
}
 
executor.shutdown();

Because its thread creation is not bounded by a fixed pool size, it should be used carefully for workloads where task submission can grow significantly.

Scheduled Executor Service

ScheduledExecutorService is used when tasks need to execute:

  • After a delay
  • At a fixed rate
  • With a fixed delay between executions

Example

ScheduledExecutorService executor =
        Executors.newScheduledThreadPool(2);

Execute a Task After a Delay

executor.schedule(
    () -> System.out.println("Task executed"),
    5,
    TimeUnit.SECONDS
);

The task is scheduled to execute after approximately five seconds.

Execute a Task Periodically

executor.scheduleAtFixedRate(
    () -> System.out.println("Running task"),
    2,
    5,
    TimeUnit.SECONDS
);

Here:

Initial delay = 2 seconds
Period        = 5 seconds

The task is scheduled repeatedly according to the specified period.

Fixed Rate vs Fixed Delay

There are two commonly used scheduling methods:

  • scheduleAtFixedRate()
  • scheduleWithFixedDelay()

scheduleAtFixedRate()

Attempts to maintain a regular schedule based on the configured period.

Start → wait period → Start → wait period → Start

scheduleWithFixedDelay()

Waits for the previous execution to finish and then waits for the specified delay before starting the next execution.

Start → Finish → wait delay → Start → Finish

This difference is important when task execution can take a significant amount of time.

invokeAll()

invokeAll() submits multiple Callable tasks and returns a list of Future objects.

Example

List<Callable<Integer>> tasks = List.of(
    () -> 10,
    () -> 20,
    () -> 30
);
 
List<Future<Integer>> results =
        executor.invokeAll(tasks);

We can retrieve the results:

for (Future<Integer> future : results) {
    System.out.println(future.get());
}

Output

10
20
30

invokeAll() waits for all submitted tasks to complete before returning, unless the timed form is used.

invokeAny()

invokeAny() submits multiple Callable tasks and returns the result of one successfully completed task.

Example

List<Callable<String>> tasks = List.of(
    () -> "Task 1",
    () -> "Task 2",
    () -> "Task 3"
);
 
String result = executor.invokeAny(tasks);
 
System.out.println(result);

The exact task that supplies the result can vary depending on execution timing.

ThreadPoolExecutor

ThreadPoolExecutor is a configurable implementation of ExecutorService.

It allows us to control important thread-pool properties such as:

  • Core thread count
  • Maximum thread count
  • Keep-alive time
  • Work queue
  • Thread factory
  • Rejected execution policy

Example

ThreadPoolExecutor executor =
        new ThreadPoolExecutor(
            2,
            5,
            60,
            TimeUnit.SECONDS,
            new LinkedBlockingQueue<>()
        );

Here:

Core threads   = 2
Maximum threads = 5
Keep alive     = 60 seconds
Work queue     = LinkedBlockingQueue

This provides much more control than the simple factory methods.

How ThreadPoolExecutor Processes Tasks

A simplified model is:

Task submitted
      |
      v
Are core threads available?
      |
      +---- Yes ----> Execute task
      |
      No
      |
      v
Is work queue available?
      |
      +---- Yes ----> Put task in queue
      |
      No
      |
      v
Can another thread be created?
      |
      +---- Yes ----> Create worker
      |
      No
      |
      v
Reject task

The exact behavior depends on the executor configuration and queue capacity.

BlockingQueue in a Thread Pool

A ThreadPoolExecutor uses a BlockingQueue<Runnable> to hold tasks waiting for execution.

For example:

new LinkedBlockingQueue<>()

Tasks can therefore be in different stages:

Submitted
    |
    v
Running
    |
    v
Completed

or:

Submitted
    |
    v
Waiting in Queue
    |
    v
Running
    |
    v
Completed

RejectedExecutionHandler

A task can be rejected when an executor cannot accept it.

ThreadPoolExecutor supports different rejection policies.

Common policies include:

  • AbortPolicy
  • CallerRunsPolicy
  • DiscardPolicy
  • DiscardOldestPolicy

For example:

new ThreadPoolExecutor.CallerRunsPolicy()

The rejection policy determines what happens when a task cannot be accepted.

ExecutorService vs Thread

Using Thread directly:

Thread thread = new Thread(() -> {
    System.out.println("Task");
});
 
thread.start();

Using ExecutorService:

ExecutorService executor =
        Executors.newFixedThreadPool(2);
 
executor.execute(() -> {
    System.out.println("Task");
});
 
executor.shutdown();

The second approach separates the task from the mechanism used to execute it.

ExecutorService vs Runnable

Runnable represents the task:

Runnable task = () -> {
    System.out.println("Task");
};

ExecutorService manages execution:

executor.execute(task);

So:

Runnable
   ↓
Represents the work
 
ExecutorService
   ↓
Manages execution

ExecutorService vs Callable

FeatureRunnableCallable
Methodrun()call()
Return valueNoYes
Checked exceptionCannot declare through run()Can throw
Common submission methodexecute() / submit()submit()
ResultFuture<?> with submit()Future<T>

Example

Runnable runnable = () -> {
    System.out.println("Running");
};
 
Callable<Integer> callable = () -> {
    return 100;
};

ExecutorService vs Executor

FeatureExecutorExecutorService
Task executionYesYes
execute()YesYes
submit()NoYes
Shutdown supportNoYes
Future supportNoYes
Batch operationsNoYes

Executor is the simpler abstraction, while ExecutorService provides additional task and lifecycle management.

Handling Exceptions in Executor Tasks

When using submit(), exceptions thrown by the task are captured and can be observed through Future.get().

Example

ExecutorService executor =
        Executors.newFixedThreadPool(2);
 
Future<?> future = executor.submit(() -> {
    throw new RuntimeException("Something went wrong");
});
 
try {
    future.get();
} catch (ExecutionException e) {
    System.out.println(
        "Task failed: " + e.getCause()
    );
} catch (InterruptedException e) {
    Thread.currentThread().interrupt();
}
 
executor.shutdown();

The exception can therefore be handled by the code retrieving the result.

Important: Always Shut Down the Executor

A common mistake is to create an executor and never shut it down.

For example:

ExecutorService executor =
        Executors.newFixedThreadPool(5);

After the required work is submitted, normally shut it down:

executor.shutdown();

Otherwise, its worker threads may continue to keep the application alive.

Executor Framework with Try-Finally

A common pattern is to ensure shutdown happens even when an exception occurs:

ExecutorService executor =
        Executors.newFixedThreadPool(3);
 
try {
    executor.submit(() -> {
        System.out.println("Task");
    });
} finally {
    executor.shutdown();
}

For more complex applications, the executor lifecycle should generally be managed by the component that owns it.

Benefits of the Executor Framework

1. Thread Reuse

Worker threads can execute multiple tasks.

2. Better Thread Management

The framework manages worker threads instead of requiring manual creation for every task.

3. Controlled Concurrency

A thread pool can limit how many tasks execute concurrently.

4. Task and Thread Separation

The application focuses on tasks rather than directly managing individual threads.

5. Asynchronous Results

Future allows tasks to return results asynchronously.

6. Task Scheduling

ScheduledExecutorService supports delayed and periodic execution.

7. Better Scalability

A properly configured executor can handle many tasks without creating an unlimited number of threads.

Common Mistakes

1. Creating an Executor and Not Shutting It Down

ExecutorService executor =
        Executors.newFixedThreadPool(5);

Remember to manage its lifecycle.

2. Creating Too Many Threads

A larger pool is not automatically faster.

Too many concurrent tasks can cause:

  • CPU contention
  • Memory usage
  • Context-switching overhead
  • Resource contention

The appropriate pool configuration depends on the workload and application resources.

3. Calling Future.get() Too Early

For example:

Future<Integer> future =
        executor.submit(task);
 
Integer result = future.get();

If the task takes a long time, the calling thread waits.

Using a timeout can sometimes be more appropriate:

future.get(5, TimeUnit.SECONDS);

4. Ignoring InterruptedException

Avoid silently ignoring interruption.

A common pattern is:

catch (InterruptedException e) {
    Thread.currentThread().interrupt();
}

Restoring the interrupted status allows higher-level code to respond appropriately.

5. Assuming Tasks Execute in Submission Order

Submitting tasks in this order:

Task 1
Task 2
Task 3

does not necessarily mean they will finish in that order when multiple worker threads are available.

Executor Framework and Virtual Threads

Modern Java also provides virtual threads, which can be created through the concurrency APIs.

For example:

ExecutorService executor =
        Executors.newVirtualThreadPerTaskExecutor();
 
try {
    executor.submit(() -> {
        System.out.println("Task");
    });
} finally {
    executor.close();
}

Virtual threads are lightweight threads designed to support large numbers of concurrent tasks, particularly tasks that spend significant time waiting on blocking I/O.

They are different from traditional platform-thread pools, so choosing between a fixed platform-thread pool and virtual threads depends on the workload and application design.

Executor Framework vs Manual Thread Creation

Manual Thread Creation

Thread thread = new Thread(() -> {
    System.out.println("Task");
});
 
thread.start();

The application directly creates and starts the thread.

Executor Framework

ExecutorService executor =
        Executors.newFixedThreadPool(3);
 
executor.execute(() -> {
    System.out.println("Task");
});
 
executor.shutdown();

The application submits the task, while the executor manages the worker threads.

Real-World Example

Suppose an application needs to process multiple files.

Without an executor:

File 1 → Thread 1
File 2 → Thread 2
File 3 → Thread 3
File 4 → Thread 4
...

With a fixed thread pool:

                Executor
                    |
              Thread Pool
             /     |     \
            /      |      \
       Thread 1 Thread 2 Thread 3
            \      |      /
             \     |     /
              File Tasks

If there are 100 files but only three worker threads, the executor can process tasks in batches rather than creating 100 application-managed threads.

Important Methods of ExecutorService

MethodDescription
execute()Executes a Runnable
submit()Submits a task and returns a Future
invokeAll()Executes multiple Callable tasks and returns their futures
invokeAny()Executes multiple Callable tasks and returns one successful result
shutdown()Starts orderly shutdown
shutdownNow()Attempts to stop running tasks and returns queued tasks
isShutdown()Checks whether shutdown has started
isTerminated()Checks whether all tasks have completed after shutdown
awaitTermination()Waits for termination

Important Points to Remember

  • The Executor Framework is available in java.util.concurrent.
  • Executor is the basic task-execution abstraction.
  • ExecutorService provides task submission and lifecycle management.
  • Runnable represents a task without a return value.
  • Callable represents a task that can return a result.
  • Future represents the result of an asynchronous computation.
  • Thread pools reuse worker threads.
  • shutdown() should normally be called when the executor is no longer needed.
  • shutdownNow() attempts interruption but does not guarantee immediate termination.
  • FixedThreadPool limits the number of worker threads.
  • SingleThreadExecutor uses one worker thread.
  • ScheduledExecutorService supports delayed and periodic execution.
  • ThreadPoolExecutor provides detailed control over thread-pool behavior.
  • Multiple tasks do not necessarily finish in submission order.
  • The appropriate executor configuration depends on the workload.
  • Modern Java also provides executors designed to work with virtual threads.

Key Takeaway

The Executor Framework provides a structured way to execute and manage concurrent tasks in Java.

Instead of manually creating a new Thread for every task, we can submit tasks to an executor:

Task
 ↓

ExecutorService ↓ Thread Pool ↓ Worker Thread ↓ Task Execution

This separates what the application needs to do from how threads are created and managed, making concurrent applications easier to control and maintain.

See you in next page....🙂

← DeadlockOverview →
← DeadlockOverview →