Async Java
Asynchronous programming is a style of writing code where a long-running operation
is started, and the main program continues its work without waiting
for that operation to finish. In Java, this is often managed using the Future
interface.
The Future
Interface: A Placeholder for Results
The Future<T>
interface is a core component of Java's concurrency utilities.
It represents the result of an asynchronous computation that may not have completed yet.
When you start a task on a background thread (e.g., using an ExecutorService
), the method returns a Future
object immediately. This object is essentially a placeholder for the eventual result (of type T
).
How to Get a Future
You typically obtain a Future
by submitting a task (a Callable
) to an ExecutorService
:
import java.util.concurrent.*;
// 1. Create a service to run background threads
ExecutorService executor = Executors.newSingleThreadExecutor();
// 2. Submit a long-running task that returns a value (Callable)
Future<Integer> futureResult = executor.submit(() -> {
System.out.println("...Starting background task (simulating 3 seconds)...");
Thread.sleep(3000);
return 42; // The final result
});
// The main thread continues running immediately.
System.out.println("Main thread is not blocked yet, doing other work...");
Future Interface Methods
Method | Purpose | Key Behavior and Limitation |
---|---|---|
get() |
Retrieves the result. | BLOCKS the calling thread indefinitely until the result is available or an exception is thrown. This is usually what asynchronous code tries to avoid. |
get(timeout, unit) |
Retrieves the result with a time limit. | BLOCKS the calling thread. Throws a TimeoutException if the result isn't ready in time. |
isDone() |
Check if task completed. | Returns true if the task has finished, whether normally, by cancellation, or with an error. |
cancel(mayInterrupt) |
Attempt to cancel the task. | Attempts to stop the running task. Returns true if the cancellation was successful. |
The main limitation of the original Future
is that retrieving the result via get()
requires the calling thread to stop and wait. For modern, non-blocking asynchronous programming, Java developers prefer the CompletableFuture
class (introduced in Java 8), which allows you to chain tasks and react to completion without blocking.
Understanding Concurrency vs. Parallelism vs. Asynchronous
These three terms are often confused. Here’s how they differ:
Term | Meaning | Requires Multiple Cores? | Practical Analogy |
---|---|---|---|
Concurrent | Many tasks making progress over overlapping time periods by rapidly switching between them. | ❌ Not necessarily (can run on one core) | One Chef juggling three orders: they chop for a minute, stir for a minute, then check the oven. All tasks are progressing over time. |
Parallel | Tasks running literally at the same instant (simultaneously) on different CPU cores. | ✅ Yes | Three Chefs working on three different orders at the exact same time on three different stations. |
Asynchronous | A programming style where the caller initiates a task and continues its own work, using a Future or callback to handle the result later. |
❌ No (It's a coding approach) | Placing a take-out order by phone: You hang up and drive to the restaurant. You don't stay on hold the whole time waiting for the food. |
Future Interface Methods
get()
: Blocks until result is available (throws checked exceptions)get(timeout, unit)
: Blocks with timeoutisDone()
: Check if task completedcancel(mayInterrupt)
: Attempt to cancel the task
Understanding Concurrency vs Parallelism vs Asynchronous
Definitions
Term | Meaning | Needs Multiple Cores? | Typical Use |
---|---|---|---|
Parallel | Tasks run literally at the same time | ✅ Yes | CPU-bound work (math, image processing) |
Concurrent | Many tasks progressing in overlapping time | ❌ Not necessarily | Task scheduling, multitasking |
Asynchronous | Task runs in background, caller doesn't wait | ❌ No | I/O-bound work (network, DB, files) |
Examples
Parallel Processing:
IntStream.range(0, 10)
.parallel()
.forEach(i -> System.out.println(i + " " + Thread.currentThread()));
Concurrent Processing:
ExecutorService pool = Executors.newFixedThreadPool(2);
pool.submit(() -> doWork("A"));
pool.submit(() -> doWork("B"));
// Tasks overlap in time, may or may not run simultaneously
Asynchronous Processing:
CompletableFuture.runAsync(() -> {
delay();
System.out.println("hello");
});
System.out.println("I don't block!"); // Runs immediately
What Does "Asynchronous" Really Mean?
Asynchronous programming is about non-blocking execution:
- You start a task
- Your thread continues with other work
- The task completes later and notifies you (via callback, promise, etc.)
Think of it like ordering coffee:
- Synchronous: Order coffee → wait → receive coffee → continue
- Asynchronous: Order coffee → do other things → get notified when ready
CompletableFuture: Modern Asynchronous Programming
Introduction
CompletableFuture
is Java's equivalent to JavaScript Promises. It has three states:
- Pending: Task is running
- Resolved: Task completed successfully
- Rejected: Task failed with an exception
Basic Usage
public class AsyncExample {
public static void main(String[] args) {
// Fire and forget
show().join(); // join() blocks until completion
// With return value
try {
String value = show2().get();
System.out.println("Returned: " + value);
} catch (InterruptedException | ExecutionException e) {
throw new RuntimeException(e);
}
}
// CompletableFuture.runAsync for void tasks
public static CompletableFuture<Void> show() {
return CompletableFuture.runAsync(() -> {
delay();
System.out.println("hello");
});
}
// CompletableFuture.supplyAsync for tasks with return values
public static CompletableFuture<String> show2() {
return CompletableFuture.supplyAsync(() -> {
delay();
return "hello";
});
}
}
Building Async APIs
class MailSender {
// Synchronous method
public void sendMail() {
delay();
System.out.println("mail sent !!!!");
}
// Asynchronous wrapper
public CompletableFuture<Void> sendMailAsync() {
return CompletableFuture.runAsync(() -> sendMail());
}
}
// Usage
var mailService = new MailSender();
var future = mailService.sendMailAsync();
System.out.println("This runs immediately");
future.join(); // Wait for completion
Chaining Operations
CompletableFuture Chaining Methods
Method | Input Lambda | Returns | Use Case |
---|---|---|---|
thenApply(fn) |
T -> U |
CompletableFuture<U> |
Transform result (sync) |
thenApplyAsync(fn) |
T -> U |
CompletableFuture<U> |
Transform result (async) |
thenAccept(consumer) |
T -> void |
CompletableFuture<Void> |
Consume result, no return |
thenAcceptAsync(consumer) |
T -> void |
CompletableFuture<Void> |
Consume result (async) |
thenRun(runnable) |
() -> void |
CompletableFuture<Void> |
Run after completion |
thenCompose(fn) |
T -> CompletableFuture<U> |
CompletableFuture<U> |
Chain async operations |
Practical Examples
public class CompletableFutureChaining {
public static void main(String[] args) {
// Transform and consume
var future = CompletableFuture.supplyAsync(() -> 12)
.thenApplyAsync(v -> v * 2) // Transform: 12 -> 24
.thenAcceptAsync(v -> {
delay();
System.out.println("Thread: " + Thread.currentThread().getName());
System.out.println("Value: " + v);
});
future.join();
}
}
thenApply vs thenCompose
Key Difference:
thenApply
: Use when your function returns a plain value (T -> U
)thenCompose
: Use when your function returns a CompletableFuture (T -> CompletableFuture<U>
)
// thenApply - transforms value
CompletableFuture.supplyAsync(() -> "User123")
.thenApply(id -> fetchProfileSync(id)) // Returns Profile
.thenAccept(System.out::println);
// thenCompose - chains futures (avoids nesting)
CompletableFuture.supplyAsync(() -> "User123")
.thenCompose(id -> fetchProfileAsync(id)) // Returns CompletableFuture<Profile>
.thenAccept(System.out::println);
Without thenCompose
, you'd get CompletableFuture<CompletableFuture<Profile>>
(nested futures).
Combining Futures
Combining Two Futures
public class CombiningFutures {
public static void main(String[] args) {
// Get price in USD
var priceInUSD = CompletableFuture.supplyAsync(() -> 39);
// Get exchange rate USD -> EGP
var exchangeRate = CompletableFuture.supplyAsync(() -> 50);
// Combine results
var finalPrice = priceInUSD.thenCombine(exchangeRate,
(price, rate) -> price * rate);
System.out.println("Final price: " + finalPrice.join());
// More complex example with string parsing
var priceString = CompletableFuture.supplyAsync(() -> "39usd");
var rate = CompletableFuture.supplyAsync(() -> 50);
var result = priceString
.thenApply(price -> {
var cleanPrice = price.replace("usd", "");
return Integer.parseInt(cleanPrice);
})
.thenCombine(rate, (price, exchangeRate) -> price * exchangeRate);
System.out.println("Parsed result: " + result.join());
}
}
Working with Multiple Futures
public class MultipleFutures {
public static void main(String[] args) {
var f1 = CompletableFuture.supplyAsync(() -> { delay(); return 1; });
var f2 = CompletableFuture.supplyAsync(() -> { delay(); return 2; });
var f3 = CompletableFuture.supplyAsync(() -> { delay(); return 3; });
var f4 = CompletableFuture.supplyAsync(() -> { delay(); return 4; });
// Wait for ALL to complete
CompletableFuture.allOf(f1, f2, f3, f4).join();
System.out.println("All completed: " + f1.join());
// Wait for ANY to complete (first wins)
var slow = CompletableFuture.supplyAsync(() -> {
delay(6); return "slow";
});
var fast = CompletableFuture.supplyAsync(() -> {
delay(1); return "fast";
});
CompletableFuture.anyOf(slow, fast)
.thenAccept(System.out::println) // Prints "fast"
.join();
}
}
Error Handling
Exception Handling Patterns
public class ErrorHandling {
public static void main(String[] args) {
// Using exceptionally for error recovery
var future = CompletableFuture.supplyAsync(() -> {
throw new IllegalStateException("Something went wrong!");
});
try {
var result = future.exceptionally(throwable -> {
System.out.println("Error: " + throwable.getMessage());
return "default_value"; // Fallback value
}).get();
System.out.println("Result: " + result); // Prints "default_value"
} catch (InterruptedException | ExecutionException e) {
System.out.println("Unexpected error: " + e.getMessage());
}
// Using handle for both success and failure
var result2 = CompletableFuture.supplyAsync(() -> {
// This might succeed or fail
if (Math.random() > 0.5) {
return "Success!";
} else {
throw new RuntimeException("Failed!");
}
}).handle((value, throwable) -> {
if (throwable != null) {
return "Error: " + throwable.getMessage();
}
return "Success: " + value;
});
System.out.println(result2.join());
}
}
Timeouts
public class TimeoutHandling {
public static void main(String[] args) {
var slowTask = CompletableFuture.supplyAsync(() -> {
delay(6); // 6 seconds
return "slow result";
});
// This will throw TimeoutException
// slowTask.orTimeout(2, TimeUnit.SECONDS).join();
// Better: provide default value on timeout
var result = slowTask.completeOnTimeout("timeout_value", 1, TimeUnit.SECONDS);
System.out.println(result.join()); // Prints "timeout_value"
}
}
Advanced Patterns
Custom Thread Pools
// CompletableFuture uses ForkJoinPool.commonPool() by default
// You can provide custom executor:
var customPool = Executors.newFixedThreadPool(4);
var future = CompletableFuture.supplyAsync(() -> "result", customPool);
Async Method Composition
public CompletableFuture<String> processUser(String userId) {
return CompletableFuture.supplyAsync(() -> userId)
.thenCompose(this::fetchUser) // CompletableFuture<User>
.thenCompose(this::enrichUserProfile) // CompletableFuture<User>
.thenApply(User::getName); // CompletableFuture<String>
}
private CompletableFuture<User> fetchUser(String id) {
return CompletableFuture.supplyAsync(() -> {
// Simulate database call
delay();
return new User(id);
});
}
private CompletableFuture<User> enrichUserProfile(User user) {
return CompletableFuture.supplyAsync(() -> {
// Simulate external API call
delay();
user.setProfile(new Profile());
return user;
});
}
Best Practices
1. Resource Management
// Always shutdown executors
ExecutorService executor = Executors.newFixedThreadPool(4);
try {
// Use executor
} finally {
executor.shutdown();
}
// Or use try-with-resources (but remember it blocks!)
try (var executor = Executors.newFixedThreadPool(4)) {
// Use executor
} // Automatically shuts down and waits
2. Exception Handling
// Always handle exceptions in async chains
CompletableFuture.supplyAsync(() -> riskyOperation())
.exceptionally(throwable -> {
log.error("Operation failed", throwable);
return defaultValue;
})
.thenAccept(this::processResult);
3. Avoid Blocking in Async Code
// BAD: Blocking in async context
CompletableFuture.runAsync(() -> {
var result = anotherFuture.join(); // Blocking!
process(result);
});
// GOOD: Chain properly
CompletableFuture.runAsync(() -> setupWork())
.thenCompose(x -> anotherFuture) // Non-blocking composition
.thenAccept(this::process);
4. Memory and Context Management
// Be careful with shared state in lambdas
// Prefer passing data through the pipeline rather than capturing
5. Testing Async Code
@Test
public void testAsync() {
var future = myAsyncMethod();
// Don't forget to wait in tests!
var result = future.join();
assertEquals("expected", result);
}
Summary
CompletableFuture provide powerful tools for asynchronous programming:
- Future/Callable interfaces allow returning values from background tasks
- CompletableFuture enables modern, composable asynchronous programming
- Proper error handling and resource management are crucial
- Understanding the difference between blocking and non-blocking operations is key
Key Takeaways
- Use CompletableFuture for new async code
- Chain operations with
thenApply
,thenCompose
,thenCombine
- Handle errors with
exceptionally
orhandle
- Always consider resource cleanup
- Test your async code properly
get() vs join()
get()
: Throws checked exceptions (InterruptedException
,ExecutionException
)join()
: Throws uncheckedCompletionException
, more convenient for most use cases