Back to Java
2026-01-299 min read

Async Parallel (Java)

Learn Async Parallel (Java) step by step with clear examples and exercises.

Why This Matters

Async parallelism in Java is a crucial concept for improving the efficiency of your applications, especially when dealing with I/O-bound tasks or complex problems that require multiple tasks to be executed concurrently. By leveraging async parallelism, you can reduce execution time and improve application responsiveness significantly.

Prerequisites

To fully understand async parallelism in Java, you should have a solid grasp of the following topics:

  1. Basic Java syntax and control structures (loops, conditionals)
  2. Object-oriented programming concepts (classes, objects, methods)
  3. Exception handling using try, catch, and finally blocks
  4. Understanding the Callable, Future, and ExecutorService interfaces
  5. Synchronization mechanisms like locks and wait/notify
  6. Concurrency issues such as race conditions and deadlocks
  7. Familiarity with multi-threading concepts and how to create and manage threads in Java
  8. Understanding the differences between synchronous and asynchronous programming
  9. Knowledge of I/O operations and their impact on performance
  10. Basic understanding of the Java memory model and volatile variables

Core Concept

Java offers several ways to execute tasks concurrently, including:

  1. Threads: Java's built-in Thread class allows you to create and manage threads directly. Each thread represents a separate flow of execution within the program.
  2. ExecutorService: A higher-level abstraction over threads, providing methods for submitting tasks, managing thread pools, and executing them asynchronously. This helps reduce boilerplate code and simplifies concurrent programming.
  3. CompletableFuture: An advanced feature introduced in Java 8 that provides a more convenient way to work with asynchronous computations and handle dependencies between tasks.

ExecutorService Examples

Creating an ExecutorService

ExecutorService executor = Executors.newFixedThreadPool(5);

This creates a thread pool with a fixed number of threads (5, in this case).

Submitting Tasks

Callable<Integer> task1 = () -> {
// Your code here
};

Future<Integer> future1 = executor.submit(task1);

Here, we create a Callable object representing the task to be executed asynchronously. The submit() method returns a Future object that represents the result of the computation.

Retrieving Results

try {
Integer result = future1.get();
} catch (InterruptedException | ExecutionException e) {
// Handle exceptions here
}

The get() method blocks until the task completes and returns its result. If an exception occurs during execution, it will be caught by the try-catch block.

Shutting Down the ExecutorService

executor.shutdown();
while (!executor.isTerminated()) {
// Wait for all tasks to complete
}

Shutdown the executor service and wait for all submitted tasks to complete before terminating the program.

CompletableFuture Examples

Creating a CompletableFuture

CompletableFuture<Integer> future2 = CompletableFuture.supplyAsync(() -> {
// Your code here
});

Creates a CompletableFuture object representing an asynchronous computation.

Chaining CompletableFutures

CompletableFuture<Integer> future3 = CompletableFuture.supplyAsync(() -> {
// Your code here
}).thenApplyAsync((result) -> {
// Process the result and return a new result
});

Chains two asynchronous computations together, with the second computation processing the result of the first one.

Handling Exceptions in CompletableFutures

CompletableFuture<Integer> future4 = CompletableFuture.supplyAsync(() -> {
// Your code here
}).handle((result, throwable) -> {
if (throwable != null) {
// Handle exceptions here
} else {
// Process the result and return a new result
}
});

Handles exceptions that may occur during the computation and provides an alternative way to process the result or handle errors.

Worked Example

Let's implement an async parallel solution for finding the sum of Fibonacci numbers up to a given limit using an ExecutorService.

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;

public class AsyncFibonacci {
public static void main(String[] args) throws InterruptedException, ExecutionException {
int limit = 100;
ExecutorService executor = Executors.newFixedThreadPool(2);

List<CompletableFuture<Long>> futures = new ArrayList<>();

// Create and submit tasks to the executor service
for (int i = 1; i <= limit / 2; ++i) {
Callable<Long> task = () -> fibonacci(i);
futures.add(CompletableFuture.supplyAsync(task));
}

// Combine results from the CompletableFutures using reduce
CompletableFuture<Long> totalFuture = futures.stream()
.reduce((future1, future2) -> future1.thenCombine(future2, (result1, result2) -> result1 + result2))
.orElse(CompletableFuture.completedFuture(0L));

// Wait for the total result and print it
long total = totalFuture.get();
System.out.println("Sum of Fibonacci numbers up to " + limit + ": " + total);

executor.shutdown();
}

private static long fibonacci(int n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
}

In this example, we create an ExecutorService with 2 threads and submit tasks to calculate Fibonacci numbers up to half the given limit. We then combine the results from the CompletableFutures using the reduce() method and print the total sum.

Common Mistakes

  1. Not shutting down the ExecutorService or CompletionService: Forgetting to shutdown the ExecutorService or CompletionService can lead to memory leaks or other resource issues. Always call shutdown() before exiting the program.
  2. Ignoring exceptions: Failing to handle exceptions when submitting tasks or retrieving results can cause your program to crash unexpectedly. Use try-catch blocks to handle exceptions gracefully.
  3. Misusing thread pools: Using too few threads may result in underutilization of resources, while using too many threads can lead to excessive context switching and performance degradation. Choose an appropriate number of threads based on the specific use case.
  4. Shared state issues: Concurrent access to shared data can lead to race conditions or deadlocks. Use synchronization mechanisms like locks and wait/notify to ensure safe concurrent access to shared resources.
  5. Not properly handling task dependencies: When submitting tasks that have dependencies, make sure to manage them appropriately to avoid issues such as race conditions or deadlocks.
  6. Improper use of Future objects or CompletableFutures: Misusing Future objects or CompletableFutures can lead to exceptions or resource leaks. Always ensure you handle exceptions and properly close resources when working with Futures or CompletableFutures.
  7. Not considering thread safety: When using shared data, make sure to consider thread safety and use appropriate synchronization mechanisms to avoid issues such as race conditions or deadlocks.
  8. Using ExecutorService for CPU-bound tasks: Using ExecutorService for CPU-bound tasks can lead to performance degradation due to excessive context switching. Consider using threads directly for CPU-bound tasks instead of an ExecutorService.
  9. Not properly configuring thread pools: Properly configure thread pools by setting appropriate parameters such as the core pool size, maximum pool size, keep-alive time, and queue capacity to optimize performance based on the specific use case.
  10. Ignoring the Java memory model and volatile variables: Failing to consider the Java memory model and using non-volatile variables inappropriately can lead to race conditions or other concurrency issues. Use volatile variables judiciously when necessary.

Practice Questions

  1. Implement an async parallel solution for finding the sum of prime numbers up to a given limit using an ExecutorService.
  2. Write a program that finds the maximum common divisor (MCD) of two large numbers using an ExecutorService and the Euclidean algorithm.
  3. Implement a concurrent solution for sorting an array using the merge sort algorithm and an ExecutorService.
  4. Design an async parallel implementation of a web crawler that fetches multiple web pages simultaneously using an ExecutorService.
  5. Implement an async parallel solution for computing the Fibonacci numbers up to a given limit, but instead of summing them, find their product using an ExecutorService.
  6. Write a program that finds the shortest path in a graph using Dijkstra's algorithm and an ExecutorService to explore multiple paths concurrently.
  7. Implement an async parallel solution for performing a large number of database queries using JDBC and an ExecutorService to improve performance.
  8. Design an async parallel implementation of a file compression utility that reads multiple files simultaneously and compresses them using an ExecutorService.
  9. Write a program that finds the largest palindrome in a given text using an ExecutorService to search for palindromes concurrently.
  10. Implement an async parallel solution for solving a Sudoku puzzle using an ExecutorService to explore multiple solutions concurrently.

FAQ

  1. Why is async parallelism important in Java?

Async parallelism allows you to improve efficiency and reduce execution time by performing multiple tasks concurrently. This is particularly useful for I/O-bound tasks, improving application responsiveness, and solving complex problems more efficiently.

  1. What are the advantages of using ExecutorService instead of managing threads directly?

Using ExecutorService provides several benefits over managing threads directly: it simplifies concurrent programming by handling thread pool management, reduces boilerplate code, and offers higher-level abstractions for submitting tasks and executing them asynchronously.

  1. How can I handle exceptions when using Future objects in Java?

You can handle exceptions when using Future objects by wrapping the submission of tasks in a try-catch block. The get() method will throw an ExecutionException if an exception occurs during execution, which can be caught and handled gracefully.

  1. What are some common mistakes to avoid when working with async parallelism in Java?

Common mistakes include forgetting to shut down the ExecutorService or CompletionService, ignoring exceptions, misusing thread pools, shared state issues due to concurrent access to shared resources, not properly handling task dependencies, improper use of Future objects, and not considering thread safety. Use synchronization mechanisms like locks and wait/notify to ensure safe concurrent access to shared resources.

  1. How can I optimize the performance of my async parallel Java code?

To optimize the performance of your async parallel Java code, consider the following tips:

  • Choose an appropriate number of threads based on the specific use case and available system resources.
  • Use synchronization mechanisms like locks and wait/notify to ensure safe concurrent access to shared resources.
  • Properly handle exceptions and resources when working with Futures or CompletableFutures.
  • Optimize I/O-bound tasks by using non-blocking I/O or asynchronous I/O libraries.
  • Profile your code to identify bottlenecks and optimize performance accordingly.
  1. What are the differences between ExecutorService, CompletionService, and CompletableFuture in Java?

ExecutorService is a higher-level abstraction over threads that provides methods for submitting tasks, managing thread pools, and executing them asynchronously. CompletionService is an extension of ExecutorService that allows you to retrieve the results of completed tasks in FIFO order using Future objects. CompletableFuture is an advanced feature introduced in Java 8 that provides a more convenient way to work with asynchronous computations and handle dependencies between tasks.

  1. How can I use ExecutorService for CPU-bound tasks?

ExecutorService is not recommended for CPU-bound tasks due to excessive context switching, which can lead to performance degradation. Consider using threads directly for CPU-bound tasks instead of an ExecutorService.

  1. What are some best practices for configuring thread pools in Java?

Some best practices for configuring thread pools include setting appropriate parameters such as the core pool size, maximum pool size, keep-alive time, and queue capacity based on the specific use case to optimize performance. Additionally, consider using a fixed thread pool when possible, as it offers better predictability and control over resource usage compared to other thread pool types.

  1. What are volatile variables in Java and why are they important?

Volatile variables in Java are used to ensure that changes made to the variable by one thread are immediately visible to other threads. This is useful for synchronization purposes when working with shared data. Volatile variables help avoid race conditions and improve performance by avoiding unnecessary cache updates.

  1. What is the Java memory model and why is it important?

The Java memory model defines

Async Parallel (Java) | Java | XQA Learn