Advanced Concurrency in Java
Master Java's advanced concurrency — ExecutorService, CompletableFuture, Fork/Join framework, BlockingQueue, and concurrent data structures.
ExecutorService
ExecutorService decouples task submission from thread management:
import java.util.concurrent.*;
import java.util.*;
public class ExecutorDemo {
public static void main(String[] args) throws Exception {
// Fixed pool — good for CPU-bound work (threads = CPU cores)
int cores = Runtime.getRuntime().availableProcessors();
ExecutorService pool = Executors.newFixedThreadPool(cores);
// Submit tasks and collect futures
List<Future<Integer>> futures = new ArrayList<>();
for (int i = 0; i < 10; i++) {
final int taskId = i;
futures.add(pool.submit(() -> {
// Simulate work
Thread.sleep(100);
return taskId * taskId;
}));
}
// Collect results
for (Future<Integer> f : futures) {
System.out.println(f.get()); // blocks until result is ready
}
// Always shut down the pool
pool.shutdown();
if (!pool.awaitTermination(30, TimeUnit.SECONDS)) {
pool.shutdownNow();
}
}
}
ExecutorService Factory Methods
// Fixed pool — predictable resource usage
ExecutorService fixed = Executors.newFixedThreadPool(8);
// Cached pool — creates threads on demand, reuses idle ones (can grow unbounded)
ExecutorService cached = Executors.newCachedThreadPool();
// Single-thread — sequential execution, tasks queued
ExecutorService single = Executors.newSingleThreadExecutor();
// Scheduled — run tasks after a delay or on a schedule
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(2);
scheduler.scheduleAtFixedRate(() -> checkHealth(), 0, 30, TimeUnit.SECONDS);
// Virtual thread per task (Java 21) — best for I/O-bound work
ExecutorService vtp = Executors.newVirtualThreadPerTaskExecutor();
invokeAll and invokeAny
List<Callable<String>> tasks = List.of(
() -> fetchFromServiceA(),
() -> fetchFromServiceB(),
() -> fetchFromServiceC()
);
// invokeAll — wait for ALL tasks, returns List<Future<T>>
List<Future<String>> all = pool.invokeAll(tasks, 10, TimeUnit.SECONDS);
for (Future<String> f : all) {
if (!f.isCancelled()) System.out.println(f.get());
}
// invokeAny — return the FIRST successful result, cancel the rest
String fastest = pool.invokeAny(tasks, 10, TimeUnit.SECONDS);
System.out.println("Fastest: " + fastest);
CompletableFuture
CompletableFuture composes async operations into pipelines:
import java.util.concurrent.*;
import java.util.function.*;
public class CompletableFutureDemo {
// Simulated async operations
static CompletableFuture<User> fetchUser(long id) {
return CompletableFuture.supplyAsync(() -> {
sleep(50);
return new User(id, "Alice");
});
}
static CompletableFuture<List<Order>> fetchOrders(long userId) {
return CompletableFuture.supplyAsync(() -> {
sleep(80);
return List.of(new Order("ORD-1", userId, 99.99));
});
}
public static void main(String[] args) throws Exception {
// 1. Basic async computation
CompletableFuture<String> cf = CompletableFuture
.supplyAsync(() -> "hello")
.thenApply(String::toUpperCase)
.thenApply(s -> s + "!");
System.out.println(cf.get()); // HELLO!
// 2. Sequential composition with thenCompose (flatMap equivalent)
CompletableFuture<List<Order>> pipeline = fetchUser(42)
.thenCompose(user -> fetchOrders(user.id())); // runs after fetchUser
System.out.println(pipeline.get());
// 3. Combine two independent futures with thenCombine
CompletableFuture<User> userFuture = fetchUser(42);
CompletableFuture<UserProfile> profileFuture = fetchProfile(42);
CompletableFuture<UserPage> page = userFuture.thenCombine(
profileFuture,
(user, profile) -> new UserPage(user, profile)
);
// 4. Wait for ALL futures
CompletableFuture<Void> allDone = CompletableFuture.allOf(
fetchUser(1), fetchUser(2), fetchUser(3)
);
allDone.get(); // blocks until all complete
// 5. Wait for ANY future (first to complete)
CompletableFuture<Object> anyDone = CompletableFuture.anyOf(
fetchFromRegionA(), fetchFromRegionB()
);
Object result = anyDone.get();
// 6. Error handling
CompletableFuture<User> withFallback = fetchUser(42)
.exceptionally(ex -> {
System.err.println("Fetch failed: " + ex.getMessage());
return new User(-1, "Guest"); // fallback value
});
// 7. whenComplete — runs after success OR failure
fetchUser(42).whenComplete((user, ex) -> {
if (ex != null) System.err.println("Error: " + ex);
else System.out.println("Got user: " + user);
});
// 8. handle — like exceptionally but always runs
CompletableFuture<String> handled = fetchUser(42)
.handle((user, ex) -> ex != null ? "error" : user.name());
}
static void sleep(long ms) {
try { Thread.sleep(ms); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
}
}
Parallel HTTP Calls with CompletableFuture
import java.net.http.*;
import java.net.URI;
import java.util.*;
import java.util.concurrent.*;
import java.util.stream.*;
public class ParallelRequests {
static final HttpClient HTTP = HttpClient.newHttpClient();
static CompletableFuture<String> fetch(String url) {
return HTTP.sendAsync(
HttpRequest.newBuilder(URI.create(url)).build(),
HttpResponse.BodyHandlers.ofString()
).thenApply(HttpResponse::body);
}
public static void main(String[] args) throws Exception {
List<String> urls = List.of(
"https://api.example.com/users/1",
"https://api.example.com/users/2",
"https://api.example.com/users/3"
);
// Fire all requests simultaneously
List<CompletableFuture<String>> futures = urls.stream()
.map(ParallelRequests::fetch)
.toList();
// Collect all results
List<String> responses = CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]))
.thenApply(v -> futures.stream().map(CompletableFuture::join).collect(Collectors.toList()))
.get(10, TimeUnit.SECONDS);
responses.forEach(System.out::println);
}
}
Fork/Join Framework
Designed for divide-and-conquer recursive tasks:
import java.util.concurrent.*;
public class ForkJoinDemo {
// RecursiveTask<T> — returns a value
static class ParallelSum extends RecursiveTask<Long> {
private static final int THRESHOLD = 10_000;
private final long[] array;
private final int from, to;
ParallelSum(long[] array, int from, int to) {
this.array = array; this.from = from; this.to = to;
}
@Override
protected Long compute() {
int size = to - from;
if (size <= THRESHOLD) {
// Base case — do the work directly
long sum = 0;
for (int i = from; i < to; i++) sum += array[i];
return sum;
}
// Recursive case — split in half
int mid = from + size / 2;
ParallelSum left = new ParallelSum(array, from, mid);
ParallelSum right = new ParallelSum(array, mid, to);
left.fork(); // submit left subtask asynchronously
long rightResult = right.compute(); // compute right in this thread
long leftResult = left.join(); // wait for left result
return leftResult + rightResult;
}
}
public static void main(String[] args) throws Exception {
long[] data = new long[1_000_000];
for (int i = 0; i < data.length; i++) data[i] = i + 1;
ForkJoinPool pool = ForkJoinPool.commonPool();
long sum = pool.invoke(new ParallelSum(data, 0, data.length));
System.out.println("Sum: " + sum); // 500000500000
// Parallel streams use the common ForkJoinPool implicitly
long sum2 = java.util.Arrays.stream(data).parallel().sum();
System.out.println("Stream sum: " + sum2); // same result
}
}
BlockingQueue — Producer-Consumer
import java.util.concurrent.*;
public class ProducerConsumer {
record Task(int id, String payload) {}
public static void main(String[] args) throws InterruptedException {
BlockingQueue<Task> queue = new LinkedBlockingQueue<>(100); // bounded
// Producer — puts tasks into the queue
Thread producer = Thread.ofVirtual().start(() -> {
try {
for (int i = 0; i < 20; i++) {
queue.put(new Task(i, "data-" + i)); // blocks if full
System.out.println("Produced task " + i);
Thread.sleep(50);
}
// Poison pill — signal consumers to stop
for (int c = 0; c < 3; c++) queue.put(new Task(-1, "STOP"));
} catch (InterruptedException e) { Thread.currentThread().interrupt(); }
});
// 3 consumers
for (int c = 0; c < 3; c++) {
final int consumerId = c;
Thread.ofVirtual().start(() -> {
try {
while (true) {
Task task = queue.take(); // blocks if empty
if (task.id() == -1) break; // stop on poison pill
System.out.println("Consumer " + consumerId + " processed task " + task.id());
Thread.sleep(100);
}
} catch (InterruptedException e) { Thread.currentThread().interrupt(); }
});
}
producer.join();
}
}
Concurrent Data Structures
import java.util.concurrent.*;
import java.util.concurrent.atomic.*;
// ConcurrentHashMap — thread-safe, no full locking
ConcurrentMap<String, Integer> wordCount = new ConcurrentHashMap<>();
wordCount.merge("hello", 1, Integer::sum); // atomic increment
wordCount.compute("world", (k, v) -> v == null ? 1 : v + 1);
wordCount.computeIfAbsent("java", k -> 0);
// CopyOnWriteArrayList — reads are lock-free (copy on write)
// Use when reads >> writes (e.g., listener lists)
CopyOnWriteArrayList<String> listeners = new CopyOnWriteArrayList<>();
listeners.add("listener1");
for (String l : listeners) { /* safe even if listeners is modified elsewhere */ }
// Atomic variables — lock-free single-variable operations
AtomicInteger counter = new AtomicInteger(0);
counter.incrementAndGet();
counter.addAndGet(5);
counter.compareAndSet(5, 10); // CAS — atomic if 5, set to 10
AtomicLong requestCount = new AtomicLong();
LongAdder hotCounter = new LongAdder(); // better than AtomicLong for high-contention counting
hotCounter.increment();
long total = hotCounter.sum();
// AtomicReference — atomic object reference update
AtomicReference<String> config = new AtomicReference<>("v1");
config.compareAndSet("v1", "v2"); // atomic swap
// Semaphore — limit concurrent access
Semaphore permits = new Semaphore(10); // allow 10 threads at once
permits.acquire();
try { callExternalApi(); }
finally { permits.release(); } Frequently Asked Questions
When should I use CompletableFuture vs virtual threads?
CompletableFuture is well-suited for composing asynchronous pipelines — chaining dependent async steps, combining multiple futures, handling errors in the pipeline. Virtual threads (Java 21) let you write blocking I/O code that reads like synchronous code while still being highly scalable. For new code on Java 21+, virtual threads with structured concurrency are often simpler; for complex async pipelines, CompletableFuture remains useful.
What is the difference between submit() and execute() on ExecutorService?
execute(Runnable) fires and forgets — it returns void and unchecked exceptions are passed to the thread's UncaughtExceptionHandler. submit(Callable) returns a Future<T> you can use to get the result or exception. Always prefer submit() so you have a handle to the task and can retrieve exceptions.
What is the difference between ConcurrentHashMap and synchronizedMap?
Collections.synchronizedMap wraps a HashMap with a single mutex — one thread at a time for all operations. ConcurrentHashMap uses lock striping (Java 8: CAS + bin-level locks) — multiple threads can read/write different parts concurrently. ConcurrentHashMap is almost always the right choice for concurrent access.
When should I use ForkJoinPool?
ForkJoinPool is ideal for recursive, divide-and-conquer problems that split into independent subtasks: parallel sorting, tree traversal, image processing, recursive algorithms. It uses work-stealing — idle threads steal tasks from busy threads' queues. Parallel streams use the common ForkJoinPool internally.