Asynchronous Programming in Java

Introduction

Asynchronous programming is a form of parallel programming that allows a unit of work to run separately from the primary application thread. When the work is complete, it notifies the main thread or handles the result in a callback.

Futures and CompletableFutures

Future

The Future interface represents the result of an asynchronous computation. It provides methods to check if the computation is complete, to wait for its completion, and to retrieve the result.

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

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

        Callable<Integer> task = () -> {
            Thread.sleep(1000);
            return 123;
        };

        Future<Integer> future = executor.submit(task);

        try {
            Integer result = future.get(); // Blocking call
            System.out.println("Result: " + result);
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        } finally {
            executor.shutdown();
        }
    }
}

CompletableFuture

CompletableFuture is a more flexible and powerful way to handle asynchronous computations. It allows you to attach callbacks to the future, handle exceptions, and compose multiple futures.

Java
import java.util.concurrent.CompletableFuture;

public class CompletableFutureExample {
    public static void main(String[] args) {
        CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return 123;
        });

        future.thenAccept(result -> System.out.println("Result: " + result));

        System.out.println("Main thread continues to run...");
        
        try {
            // Block and wait for the result to prevent the program from exiting immediately
            future.get();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

Advanced Features

Combining CompletableFutures:
You can combine multiple CompletableFuture instances using methods like thenCombine, thenCompose, and allOf.

Java
import java.util.concurrent.CompletableFuture;

public class CombineCompletableFutures {
    public static void main(String[] args) {
        CompletableFuture<Integer> future1 = CompletableFuture.supplyAsync(() -> 50);
        CompletableFuture<Integer> future2 = CompletableFuture.supplyAsync(() -> 70);

        CompletableFuture<Integer> combinedFuture = future1.thenCombine(future2, Integer::sum);
        combinedFuture.thenAccept(result -> System.out.println("Combined Result: " + result));

        try {
            // Block and wait for the result to prevent the program from exiting immediately
            combinedFuture.get();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

Exception Handling

CompletableFuture provides methods to handle exceptions using handle, exceptionally, and whenComplete.

Java
import java.util.concurrent.CompletableFuture;

public class CompletableFutureExceptionHandling {
    public static void main(String[] args) {
        CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
            if (true) {
                throw new RuntimeException("Something went wrong");
            }
            return 42;
        });

        future.exceptionally(ex -> {
            System.out.println("Exception: " + ex.getMessage());
            return 0;
        }).thenAccept(result -> System.out.println("Result: " + result));

        try {
            // Block and wait for the result to prevent the program from exiting immediately
            future.get();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

Conclusion

Multi-threading and asynchronous programming are essential concepts in Java that help improve application performance and responsiveness. Understanding how to create and manage threads, synchronize resources, and utilize Future and CompletableFuture for asynchronous tasks is crucial for developing efficient Java applications. With these tools and techniques, you can write scalable, high-performance code that leverages the full power of modern multi-core processors.

Scroll to Top