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.
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.
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.
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.
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.