Java > Concurrency and Multithreading > Executors and Thread Pools > ExecutorService

Basic ExecutorService Example: Submitting Tasks

ExecutorService provides a powerful way to manage threads and execute tasks asynchronously. This example demonstrates how to submit tasks to an ExecutorService and retrieve results using Future objects. This is a fundamental example for understanding thread pool management.

Code Snippet

This code creates a fixed-size thread pool using Executors.newFixedThreadPool(2). A Callable represents a task that returns a value. The executor.submit(task) method submits the task to the thread pool and returns a Future object. The Future.get() method blocks until the task completes and returns the result. Finally, executor.shutdown() initiates an orderly shutdown of the executor, allowing previously submitted tasks to complete.

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.Callable;

public class ExecutorServiceExample {

    public static void main(String[] args) throws Exception {
        // Create a fixed-size thread pool with 2 threads
        ExecutorService executor = Executors.newFixedThreadPool(2);

        // Define a task that returns a value
        Callable<String> task = () -> {
            System.out.println("Task started by thread: " + Thread.currentThread().getName());
            Thread.sleep(2000); // Simulate some work
            return "Task completed";
        };

        // Submit the task to the executor and get a Future
        Future<String> future = executor.submit(task);

        // Perform other operations while the task is running
        System.out.println("Main thread doing other work: " + Thread.currentThread().getName());

        // Get the result of the task (this will block until the task is complete)
        String result = future.get();
        System.out.println("Result: " + result);

        // Shutdown the executor
        executor.shutdown();
    }
}

Concepts Behind the Snippet

ExecutorService decouples task submission from task execution. It manages a pool of threads, reusing them to execute multiple tasks, which reduces the overhead of creating new threads for each task. Callable is an interface similar to Runnable, but it can return a value and throw checked exceptions. Future represents the result of an asynchronous computation. It provides methods to check if the computation is complete, wait for its completion, and retrieve the result.

Real-Life Use Case

In a web server, handling multiple client requests concurrently. Each request can be submitted as a task to an ExecutorService, allowing the server to process multiple requests in parallel without blocking the main thread. This improves responsiveness and throughput.

Best Practices

  • Always shut down the ExecutorService after use to release resources. Use shutdown() for a graceful shutdown or shutdownNow() for an immediate shutdown.
  • Handle exceptions within the tasks to prevent them from propagating and potentially crashing the application.
  • Choose the appropriate thread pool size based on the nature of the tasks and the available resources.
  • Consider using a try-with-resources block to automatically shutdown the ExecutorService.

Interview Tip

Be prepared to discuss the different types of thread pools available through the Executors factory class (e.g., fixed-size, cached, scheduled). Also, explain the difference between shutdown() and shutdownNow().

When to Use Them

Use ExecutorService when you need to execute tasks asynchronously and manage a pool of threads efficiently. It's particularly useful for CPU-bound tasks or I/O-bound tasks that can be executed concurrently.

Memory Footprint

The memory footprint depends on the size of the thread pool and the tasks being executed. A larger thread pool consumes more memory. Careful consideration should be given to sizing the thread pool based on available resources.

Alternatives

Alternatives to ExecutorService include using raw threads (Thread class), but this is generally discouraged due to the complexity of managing threads manually. Another alternative for simpler concurrency scenarios is using the CompletableFuture API.

Pros

  • Improved performance due to thread reuse.
  • Simplified thread management.
  • Decoupling of task submission and execution.
  • Easy to control the degree of concurrency.

Cons

  • Potential for thread starvation if tasks are not properly designed.
  • Risk of deadlocks if tasks synchronize on shared resources incorrectly.
  • Overhead of managing the thread pool.

FAQ

  • What is the difference between Runnable and Callable?

    Runnable is a functional interface that represents a task that does not return a value. Callable is similar, but it can return a value and throw checked exceptions.
  • What happens if I don't call executor.shutdown()?

    The application may not terminate because the non-daemon threads in the thread pool will continue to run, waiting for new tasks. This can lead to resource leaks and prevent the JVM from exiting.
  • How do I handle exceptions thrown by tasks submitted to the ExecutorService?

    Wrap the task's code in a try-catch block to catch and handle exceptions. You can log the exception, retry the task, or perform other error handling actions. Remember to deal with exceptions inside the Callable or Runnable's call() or run() method respectively.