Spring Boot Handbook

    MultiThreading: Java Executor Framework

    Introduction#

    Java multithreading is a powerful mechanism that enables concurrent execution of several tasks with great performance improvement, responsiveness, and resource usage. Nevertheless, using the Thread class or the Runnable interface to manage threads manually poses issues like high memory usage, intricate lifecycle management, and possible performance bottlenecks. In response to these problems, Java brought in the Executor Framework, a high-level API for handling multithreading efficiently without the need to explicitly create and manage threads. The Java Executor Framework offers an elegant and scalable way of executing tasks using thread pools, by minimizing overhead and maximizing CPU utilization. It features major components such as ExecutorService, ThreadPoolExecutor, and ScheduledExecutorService, which simplify thread management by taking care of submitting tasks, executing tasks, and shutting down. The framework provides various thread pool types, including FixedThreadPool, CachedThreadPool, SingleThreadExecutor, and ScheduledThreadPool, based on application requirements. With the use of the Executor Framework, Java developers can implement high-performance concurrency without too much complexity, and it has become a must-have resource for developing scalable applications, managing parallel processing, and running background operations optimally. Java provides its multithreading framework called the Java Executor Framework which is introduced in JDK5 in java.util.concurrent package.

    Java Executor Framework

    Executor Interface#

    The executor interface in the Java executor framework provides a very easy way of doing multithreading since task execution is done through thread-pooling, as opposed to creating the thread manually. It has the method execute(Runnable command) which provides the scheduling of the tasks for future execution on a new thread, or a pooled thread, or on the calling thread itself depending on the implementation. Such an approach is very beneficial in terms of performance, resource optimization, and efficient thread management. Thus, providing an essential tool for building scalable and high-performance Java applications.

    Executor Interface

    ExecutorService Interface#

    In addition to the Executor interface, the ExecutorService interface provides more advanced thread management with submit() supporting both the Runnable (no result) and Callable<T> (returns a result). This is called the asynchronous execution, having the Future<T> to retrieve task results, managing threads pools, including scheduling. An ExecutorService is a major component for building scalable and high-performance multithreaded applications due to features such as task- and life-cycle management.

    ThreadPoolExecutor Class#

    ThreadPoolExecutor is one such class in Java that extends the flexibility of ExecutorService concerning the efficient management of worker threads. ThreadPoolExecutor allows fine-grained control over core pool size, maximum pool size, task queueing, and thread keep-alive time. Submitted tasks will either be executed immediately or taken into an internal queue, or else they will be rejected when thread availability and queue capacity don't allow immediate execution. In a nutshell, it maintains the flexibility to optimize concurrency through dynamic scaling, task queueing strategies, and rejection policies, which ideally suits applications requiring high performance and scalability.

    Creating Thread Pool#

    Thread pools in Java can be formed using Executors or directly through ThreadPoolExecutor. The Executors class provides multiple factory methods for creating popular thread pools such as fixed thread pools (Executors.newFixedThreadPool(n)), scheduled thread pools (Executors.newScheduledThreadPool(n)), and single-thread executors (Executors.newSingleThreadExecutor()). On the other hand, ThreadPoolExecutor is a specialized class suited for scaling applications to high performance and fine-tuning the core threads, maximum number of threads, queue, and rejection policies.

    Creating Thread Pool

    Create via ThreadPoolExecutor class:#

    import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.LinkedBlockingDeque; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; public class LearnJavaExecutor { public static void main(String[] args) { ThreadPoolExecutor executor = new ThreadPoolExecutor(4,6,2, TimeUnit.SECONDS,new ArrayBlockingQueue<>(10)); System.out.println("Starting main thread " + Thread.currentThread().getName()); //Submitting 100 Tasks to the Executor for (int i=0;i<100;i++) { int finalI = i; executor.submit(new Runnable() { @Override public void run() { System.out.println(finalI + " Starting Tasks" + Thread.currentThread().getName()); try { Thread.sleep(40000); } catch (InterruptedException e) { throw new RuntimeException(e); } System.out.println(finalI+" Ending Tasks" + Thread.currentThread().getName()); } }); } System.out.println("Ending Tasks" + Thread.currentThread().getName()); } }

    Code Breakdown:#

    Creating a ThreadPoolExecutor

    ThreadPoolExecutor executor = new ThreadPoolExecutor(4,6,2, TimeUnit.SECONDS,new ArrayBlockingQueue<>(10));
    • Core Pool Size = 4 → The executor starts with 4 active threads.
    • Maximum Pool Size = 6 → Can create up to 6 threads if needed.
    • Keep-Alive Time = 2 seconds → Extra threads (beyond core threads) will be terminated if idle for 2 seconds.
    • ArrayBlockingQueue<>(10) → Tasks waiting for execution are stored in a queue of size 10.

    Submitting 100 Tasks to the Executor

    for (int i=0;i<100;i++) { int finalI = i; executor.submit(new Runnable() { @Override public void run() { System.out.println(finalI + " Starting Tasks" + Thread.currentThread().getName()); try { Thread.sleep(40000); } catch (InterruptedException e) { throw new RuntimeException(e); } System.out.println(finalI+" Ending Tasks" + Thread.currentThread().getName()); } }); }
    • A loop runs 100 times, submitting 100 tasks.
    • Each task:
      • Prints a "Starting Task" message with the thread name.
      • Sleeps for 40 seconds, simulating a long-running operation.
      • Prints an "Ending Task" message after sleeping.
    1. Potential Issues
      • Task Rejection:
        • The pool can only handle 4 active threads initially, and 6 threads at most.
        • The queue can only store 10 tasks.
        • When all 16 slots (6 running threads + 10 queued tasks) are occupied, the remaining tasks will be rejected, leading to RejectedExecutionException.
      • Executor is not shut down → The application may not terminate properly.
      • Handle Task Rejection:
        • The custom RejectedExecutionHandler retries rejected tasks after a 2s delay.
    new RejectedExecutionHandler() { @Override public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) { log.info("Thread rejected... Retrying.."); try { Thread.sleep(2000); } catch (InterruptedException e) { throw new RuntimeException(e); } executor.submit(r); //retry } } );

    This program creates a fixed-size thread pool to execute 100 long-running tasks. Due to limited threads and queue size, many tasks will be rejected. To fix this, either increase the queue size, use more threads, or handle rejected tasks properly.

    Output#

    1. With Out Handle Task Rejection and Consider ScheduledExecutorService:
    RejectedExecutionException
    • 0 Starting Taskspool-1-thread-1
      • Task 0 starts execution on thread-1.
    • 0 Ending Taskspool-1-thread-1
      • Task 0 completes execution on thread-1 after sleeping for 40 seconds.
    • 7 Starting Taskspool-1-thread-1
      • Task 7 begins execution on thread-1 after a previous task finishes.
    • 7 Ending Taskspool-1-thread-1
      • Task 7 completes execution on thread-1 after its sleep time.
    1. Handle Task Rejection:
    Handle Task Rejection

    Observations#

    • Thread Reusability: The same thread-1 is handling multiple tasks sequentially.
    • Tasks are queued: Since the executor has a limited number of threads and a queue, tasks execute only when a thread is free.
    • Long Execution Time (40s sleep): Causes task backlog, leading to RejectedExecutionException when the queue gets full.
    multi-threaded execution

    The IntelliJ IDEA console shows logs of multi-threaded execution, where you searched for "Starting Task". The search results indicate:

    • 16 occurrences of "Starting Taskspool-1-thread-X", meaning 16 threads started execution.
    • You are currently viewing the 7th match out of 16 (7/16).
    • The highlighted blue text marks the search results, and the yellow/orange bar shows their distribution in the log.

    This confirms that 16 start events occurred for different threads in the Taskspool-1.

    ScheduledExecutorService Class#

    ScheduledExecutorService is a very useful extension of the ExecutorService. It allows the user to execute scheduled tasks either at particular times or at some regular intervals such that some tasks are completed on a fixed date. The advantage of using it is that it has some very useful methods, such as schedule() for delayed execution, scheduleAtFixedRate()for fixed interval execution, and scheduleWithFixedDelay(), which is delayed execution after the completion of the previous queued task in the queue. It is much better than Timer, as it does well in thread management, making it appropriate for periodic tasks, recurring updates, and maintenance of application performance in an application's background.

    Consider ScheduledExecutorService:#

    If tasks need periodic execution, ScheduledThreadPoolExecutor is a better alternative.

    import java.util.concurrent.*; public class LearnJavaExecutor { public static void main(String[] args) { ScheduledThreadPoolExecutor scheduledThreadPoolExecutor = new ScheduledThreadPoolExecutor(6, new ThreadFactory() { @Override public Thread newThread(Runnable r) { System.out.println(" "); return new Thread(r, "thread "+System.nanoTime()); } }); System.out.println("Starting main thread " + Thread.currentThread().getName()); scheduledThreadPoolExecutor.schedule(new Runnable() { @Override public void run() { System.out.println(" Starting Tasks" + Thread.currentThread().getName()); try { Thread.sleep(40000); } catch (InterruptedException e) { throw new RuntimeException(e); } System.out.println("Ending Tasks" + Thread.currentThread().getName()); } }, 4, TimeUnit.SECONDS); System.out.println("Ending Tasks" + Thread.currentThread().getName()); } }

    Output:#

    ScheduledExecutorService Output

    Conclusion#

    This article presents an overview of the Java Executor Framework with particular reference to ExecutorService, ThreadPoolExecutor, and ScheduledExecutorService as multithreading mechanisms. By controlling execution of tasks, it improves concurrency, thus minimizing resource overhead, and enhancing the performance and scalability of Java applications.

    Last updated on Apr 09, 2025