Java ExecutorService with Example

Java ExecutorService is an interface from java.util.concurrent package. It provides methods to execute asynchronous tasks, check the results of those tasks, and shutdown those running tasks when needed.

Think of ExecutorService as a manager that takes care of executing tasks for you. You submit tasks to the ExecutorService, and it handles the creation of threads and distribution of tasks among those threads. It abstracts away the complexities of manually creating and managing threads.

With ExecutorService, you can define a pool of worker threads that are available to execute tasks. As tasks are submitted to the ExecutorService, it assigns them to the available threads in the pool. The ExecutorService takes care of reusing threads to avoid the overhead of creating new threads for every task.

By using ExecutorService, you can achieve better resource utilization, improved performance, and simplified concurrency management in your Java applications. It provides a higher-level abstraction for handling concurrent tasks, allowing you to focus on the logic of your tasks rather than low-level thread management.

ExecutorService in Java can be used in various scenarios when you need to manage and execute tasks concurrently. Here are some situations where ExecutorService is commonly used:

  • Parallelizing Task Execution: When there is a set of independent tasks that can be executed concurrently, ExecutorService provides a convenient way to parallelize their execution. The tasks can be submitted to the ExecutorService, and it takes care of assigning them to available threads for concurrent execution, maximizing resource utilization and potentially improving performance.
  • Asynchronous Operations: If there are tasks that can run independently in the background while your main program continues its execution, ExecutorService can be used to execute those tasks asynchronously. This allows your program to perform other operations without waiting for the completion of each task, enabling better responsiveness and utilization of available resources.
  • Thread Pool Management: Creating and managing threads manually can be complex and resource-intensive. ExecutorService simplifies thread management by providing a thread pool. You can define the size of the thread pool based on the available resources and expected workload. The ExecutorService handles thread creation, reuse, and lifecycle management, providing a more efficient and controlled environment for executing tasks.
  • Limiting Resource Usage: ExecutorService allows you to control the number of concurrent threads executing tasks. This can be useful when you want to limit the overall resource usage, such as limiting the maximum number of threads to avoid overwhelming system resources or restricting concurrent access to certain resources like databases or network connections.
  • Handling Future Results: ExecutorService in Java lets you send tasks for execution and get the results later. When you submit a task to ExecutorService, it gives you a Future, which is like a promise for the result of that task. You can use the Future to check if the task is done, get the result when it's finished, or cancel the task if needed.


Creating an ExecutorService

We can use the Executors factory class from the java.util.concurrent package to create instances of ExecutorService. The Executors class provides several factory methods for creating instances of ExecutorService. How you want to create an instance of ExecutorService depends on what you want your code to do.

Here are a few examples of creating instances of ExecutorService in Java:

Example 1: Creating a Fixed Thread Pool ExecutorService

ExecutorService executor = Executors.newFixedThreadPool(10);

In this code, we are creating an ExecutorService named executor using the newFixedThreadPool() method from the Executors class. The newFixedThreadPool() method creates a fixed-size thread pool, meaning it creates a pool of threads that remains constant in size. In this case, the pool size is set to 10, which means it will have 10 worker threads available for executing tasks. Once the fixedPoolExecutor is created, you can submit tasks to it for execution using methods like execute() or submit(). By using a fixed thread pool ExecutorService, you can control the number of concurrent tasks being executed at any given time. It provides a balance between concurrency and resource utilization. With 10 threads in the pool, up to 10 tasks can be executed simultaneously, while any additional tasks will be queued and wait for a thread to become available.

It's important to note that you should properly handle the lifecycle of the ExecutorService. Once you're done with it, make sure to call the shutdown() method to gracefully shut down the ExecutorService and release any resources associated with it.


Example 2: Creating a Single Thread ExecutorService

ExecutorService executor = Executors.newSingleThreadExecutor();

In this code, we are creating an ExecutorService named executor using the newSingleThreadExecutor() method from the Executors class. The newSingleThreadExecutor() method creates an ExecutorService with a single thread. This means that the ExecutorService will have only one worker thread available for executing tasks. Once the singleExecutor is created, you can submit tasks to it for execution using methods like execute() or submit(). Using a single-threaded ExecutorService ensures that tasks are executed in a serialized manner, which can be useful in scenarios where you need strict sequential execution or when tasks require synchronization or shared resources that can't be accessed concurrently. It's important to note that if a task submitted to a single-threaded ExecutorService encounters an exception and terminates abruptly, a new thread will be created to replace the terminated thread, ensuring that subsequent tasks can still be executed. Once you're done with it, make sure to call the shutdown() method to gracefully shut down the ExecutorService and release any resources associated with it.


Example 3: Creating a Cached Thread Pool ExecutorService

ExecutorService chachedPoolExecutor = Executors.newCachedThreadPool();

This newCachedThreadPool() method creates an ExecutorService with a thread pool that is designed to automatically adjust its size based on the workload. It creates and maintains a pool of threads that are reused to execute tasks. This type of thread pool is useful when you have a large number of short-lived tasks and you want to efficiently manage their execution without creating too many threads upfront. It allows threads to be reused and avoids the overhead of creating new threads for each task. The number of threads in the pool can grow or shrink based on the demand for executing tasks.

It is important to note that this thread pool might not be suitable for long-running tasks or situations where you need fine-grained control over the number of threads. In those cases, other types of thread pools, such as fixed-size thread pools or single-threaded executors, may be more appropriate.


Example 4: Creating a Scheduled Thread Pool ExecutorService

ExecutorService scheduledPoolExecutor = Executors.newScheduledThreadPool(10);

This code creates a thread pool with a fixed number of threads (in this case, 10) called executor using the newScheduledThreadPool() method from the Executors class. This thread pool is specifically designed for executing tasks at scheduled intervals or with a delay.

You can schedule tasks to run at specific intervals or after a certain delay using methods like schedule(), scheduleAtFixedRate(), or scheduleWithFixedDelay().

The scheduled thread pool is useful when you need to execute tasks periodically or with a delay. It provides a convenient way to schedule tasks without the need for manual thread management.

Keep in mind that the number of threads in the pool is fixed, so if you have more tasks than available threads, some tasks may have to wait for a thread to become available. If you have a large number of scheduled tasks or tasks with long execution times, you might need to adjust the thread pool size accordingly.


ExecutorService Methods

The ExecutorService interface in Java provides various methods to execute tasks asynchronously. Here are some of the commonly used methods:

  • execute(Runnable command): Executes the given Runnable task in a new thread, in a new pooled thread, or in the calling thread. It doesn't return a result or a Future. Use this method when you don't need to track the task's progress or result.
  • Example:

    ExecutorService executor = Executors.newSingleThreadExecutor();
    executor.execute(new Runnable() {
        @Override
        public void run() {
            // your task task here
        }
    });
    executor.shutdown(); // shutting down executor

  • submit(Runnable task): Submits a Runnable task that returns a Future object representing the result of the task upon completion.
  • Example:

    ExecutorService executor = Executors.newSingleThreadExecutor();
    Future future = executor.submit(new Runnable() {
        @Override
        public void run() {
           // your task here
        }
    });
    
    try {
       future.get(); // returns null on successful completion
    } catch (InterruptedException e) {
       e.printStackTrace();
    } catch (ExecutionException e) {
       e.printStackTrace();
    } 
    		
    executor.shutdown(); //shutting down executor

  • submit(Callable task): Submits a task that returns a value. The method returns a Future object representing the result of the task upon completion.
  • Example:

    ExecutorService executorService = Executors.newSingleThreadExecutor();
    
    // Submit the task and obtain a Future object
    Future future = executorService.submit(new Callable() {
       @Override
       public Integer call() throws Exception {
          // Task code here
          return 5;
       }
    });
    
    try {
       // Get the result from the Future object
       int result = future.get();
       System.out.println("Task returned result: " + result);
    } catch (InterruptedException e) {
       e.printStackTrace();
    } catch (ExecutionException e) {
       e.printStackTrace();
    }
    
    // Shutdown the executor service
    executorService.shutdown();

  • invokeAll(Collection> tasks): This method runs a list of tasks and returns a list of Futures representing the results and status of the tasks after completely running all the given tasks.
  • Example:

    ExecutorService executorService = Executors.newFixedThreadPool(4);
    List> callableTaskList = new ArrayList<>();
    callableTaskList.add(new Callable() {
        @Override
        public String call() throws Exception {
    	return "Asynchronous task 1";
        }
    });
    
    callableTaskList.add(new Callable() {
        @Override
        public String call() throws Exception {
    	return "Asynchronous task 2";
        }
    });
    
    callableTaskList.add(new Callable() {
        @Override
        public String call() throws Exception {
    	return "Asynchronous task 3";
        }
    });		
    
    callableTaskList.add(new Callable() {
        @Override
        public String call() throws Exception {
            return "Asynchronous task 100";
        }
    });
    		
    List> futureList = null;
    try {
        futureList = executorService.invokeAll(callableTaskList);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    
    for (Future future : futureList) {
       try {
           String result = future.get();
           System.out.println("Result = " + result);
       } catch (InterruptedException | ExecutionException e) {
           e.printStackTrace();
       }
    }
    
    executorService.shutdown();

  • invokeAny(Collection> tasks): This method executes the given task and returns the result of one task that has completed successfully. Incomplete tasks are cancelled upon any error.
  • Example:

    ExecutorService executorService = Executors.newFixedThreadPool(4);
    List> callableTaskList = new ArrayList<>();
    callableTaskList.add(new Callable() {
        @Override
        public String call() throws Exception {
    	return "Asynchronous task 1";
        }
    });
    
    callableTaskList.add(new Callable() {
        @Override
        public String call() throws Exception {
    	return "Asynchronous task 2";
        }
    });
    
    callableTaskList.add(new Callable() {
        @Override
        public String call() throws Exception {
    	return "Asynchronous task 3";
        }
    });		
    
    callableTaskList.add(new Callable() {
        @Override
        public String call() throws Exception {
            return "Asynchronous task 100";
        }
    });
    		
    try {
        String result = executorService.invokeAny(callableTaskList);
        System.out.println("Result = " + result);
    } catch (InterruptedException e) {
        e.printStackTrace();
    } catch (ExecutionException e) {
        e.printStackTrace();
    }
    
    executorService.shutdown();

  • shutdown(): This method intiates an orderly shutdown and does not wait for the previously submitted tasks to complete.
  • Example:

    executorService.shutdown();

  • shutdownNow(): This method attempts to shutdown all running tasks and returns a list of all tasks that were awaiting for execution. This method does not wait for previously submitted tasks to complete.
  • Example:

    executorService.shutdownNow();

  • awaitTermination(long timeout, TimeUnit unit): This method blocks all types of termination request until all submitted tasks have completed execution.
  • Example:

    executor.shutdown();
    executor.awaitTermination(60, TimeUnit.SECONDS);

    Cancelling Task

    A running task can be cancelled by calling the cancel() method on the Future object. The cancel attempt will not work if the task has already been completed or cancelled.

    Example:

    boolean cancelled = future.cancel(true);