A Guide to Java Concurrency with Executors and Future
Mehmood Ghaffar

Mehmood Ghaffar @mgm06bm

About: I write about programming, software development, and DevOps, focusing on frontend, backend, and tutorials to simplify complex tech concepts.

Location:
Germany
Joined:
Mar 26, 2025

A Guide to Java Concurrency with Executors and Future

Publish Date: Mar 28
0 0

Java Future and Executors

A Guide to Java ExecutorService and CompletableFuture

Digital illustration represent java concurrency with glowing interconnected threads
Concurrency with Executors vs CompletableFutures

Introduction

In modern Java applications, efficient concurrency handling is crucial for performance and scalability. Java provides two powerful tools for managing concurrent tasks: ExecutorService and CompletableFuture. These APIs simplify thread management, allowing developers to execute tasks asynchronously without manually handling thread lifecycles.

In this article, we'll explore these two approaches with practical examples and best practices.

1. Using ExecutorService for Thread Management

Java's ExecutorService manages a pool of worker threads, efficiently executing submitted tasks. This approach is preferable to manually creating threads as it improves resource management.

Example: Using a Fixed Thread Pool

import java.util.concurrent.ExecutorService; 
import java.util.concurrent.Executors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class ExecutorExample {
  private Logger logger = Loggerfactory
getLogger(getClass());

  public static void main(String[] args) { 
  ExecutorService executor = Executors.newFixedThreadPool(3); 

  Runnable task1 = () -> { 
    logger.info(Thread.currentThread().getName() + " is executing Task 1"); 
  };

  Runnable task2 = () -> { 
    logger.info(Thread.currentThread().getName() + " is executing Task 2"); 
  }; 

  Runnable task3 = () -> { 
    logger.info(Thread.currentThread().getName() + " is executing Task 3"); 
  }; 

  executor.submit(task1); 
  executor.submit(task2); 
  executor.submit(task3); 

  executor.shutdown(); 

  } 
} 
Enter fullscreen mode Exit fullscreen mode

Key Takeaways:

  • Executors.newFixedThreadPool(n) creates a thread pool with n fixed worker threads.
  • submit() adds tasks to the pool for execution.
  • shutdown() ensures that the executor stops accepting new tasks and shuts down cleanly.

2. Using CompletableFuture for Asynchronous Execution

The CompletableFuture API provides a modern way to handle asynchronous operations, making code more readable and efficient.

Example: Running a Task Asynchronously

import java.util.concurrent.CompletableFuture; 
import java.util.concurrent.ExecutionException;
 import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class CompletableFutureExample {

  private Logger logger = Loggerfactory
getLogger(getClass());

  public static void main(String[] args) throws ExecutionException, InterruptedException { 
    CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> { 
      try { 
        Thread.sleep(2000); // Simulate long-running task 
      } catch (InterruptedException e) { 
        e.printStackTrace(); 
      } 
      return "Task Completed!"; 
    });

    logger.info("Performing other tasks in parallel.");

    // Block and get the result 
    String result = future.get(); 
    logger.info("Result: " + result);
    } 
  } 
Enter fullscreen mode Exit fullscreen mode

Key Takeaways:

  • supplyAsync() runs a task asynchronously in a default thread pool.
  • .get() blocks the main thread until the result is available (use .thenApply() for non-blocking operations).
  • Ideal for non-blocking, parallel execution of dependent tasks.

ExecutorService vs CompletableFuture: When to Use What?

When to Use ExecutorService?

When managing a fixed number of threads.

When running independent tasks that don’t need chaining.

For legacy systems that don’t support Java 8+ features.

When to Use CompletableFuture?

When handling dependent asynchronous tasks.

When you need non-blocking execution with better readability.

For performance optimization with parallel stream processing.

Conclusion

Mastering Java concurrency is essential for writing scalable, high-performance applications. ExecutorService is great for managing thread pools, while CompletableFuture excels at handling asynchronous tasks in a clean and efficient manner.

If you're working on a project that requires concurrency, consider mixing both approaches to get the best of both worlds—using ExecutorService for controlled thread management and CompletableFuture for efficient async processing.

Comments 0 total

    Add comment