What you'll learn
Raw threads are low-level and error-prone. Java's java.util.concurrent package gives you high-level tools — thread pools, futures, concurrent collections — that make concurrency practical and safe. This is how real applications do parallel work.
By the end you'll be able to:
- Run tasks on thread pools with the Executor framework
- Get results from background work using
CallableandFuture - Compose async pipelines with
CompletableFuture - Share data safely with concurrent collections
- Coordinate threads with locks, semaphores, and latches
- Understand game-changing virtual threads (Java 21)
The Executor framework & thread pools
Creating a new thread per task is wasteful — threads are expensive. An ExecutorService manages a reusable pool of threads: you submit tasks, and the pool runs them on available threads. You never manage threads by hand again:
Task queue
T1 · T2 · T3 · T4…
Thread pool
N reusable threads
Results
Future<T>
import java.util.concurrent.*;
ExecutorService pool = Executors.newFixedThreadPool(2);
for (int i = 1; i <= 3; i++) {
int taskId = i;
pool.submit(() ->
System.out.println("Task " + taskId + " on " +
Thread.currentThread().getName()));
}
pool.shutdown(); // no new tasks; finish the queued onesWatch out
shutdown() an executor when you're done, or its threads keep the JVM alive. Better yet, use it in a try-with-resources block (Java 19+ made ExecutorService AutoCloseable).Callable & Future
A Runnable returns nothing. A Callable returns a value (and can throw). When you submit one, you get a Future — a handle to a result that isn't ready yet. Calling future.get() blocks until the work completes:
import java.util.concurrent.*;
ExecutorService pool = Executors.newSingleThreadExecutor();
Future<Integer> future = pool.submit(() -> { // a Callable returns a value
Thread.sleep(100);
return 6 * 7;
});
System.out.println("Working...");
System.out.println("Result: " + future.get()); // blocks until ready
pool.shutdown();CompletableFuture
Future.get() blocks, which throws away the point of async. CompletableFuture lets you build non-blocking pipelines: run work in the background, then chain transformations and callbacks that fire when each stage completes — without ever blocking a thread until the end:
import java.util.concurrent.CompletableFuture;
CompletableFuture.supplyAsync(() -> 10) // run in the background
.thenApply(n -> n * 2) // then transform
.thenApply(n -> n + 1) // then transform again
.thenAccept(r -> System.out.println("Result: " + r))
.join(); // wait for the chainNote
thenCompose (chain dependent async calls), thenCombine (merge two independent results), and exceptionally (handle failures). It's Java's answer to async/await-style code.Concurrent collections
A plain HashMap corrupts if multiple threads write to it. The java.util.concurrent collections are built for sharing: ConcurrentHashMap allows safe concurrent reads and writes, and CopyOnWriteArrayList suits read-heavy lists. They're far faster than wrapping a regular collection in synchronized:
import java.util.concurrent.ConcurrentHashMap;
import java.util.Map;
Map<String, Integer> views = new ConcurrentHashMap<>();
views.put("home", 1);
// atomic, thread-safe update — safe from many threads at once
views.merge("home", 10, Integer::sum);
System.out.println(views.get("home"));Locks & coordination
Beyond synchronized, the concurrent package offers explicit tools for finer control:
ReentrantLock— like synchronized but withtryLock, timeouts, and fairnessReadWriteLock— many readers or one writer; great for read-heavy dataSemaphore— limit how many threads access a resource at onceCountDownLatch— wait until N tasks have finishedCyclicBarrier— make threads wait for each other at a rendezvous point
Tip
ReentrantLock, always unlock() in a finally block so the lock is released even if an exception is thrown — a forgotten unlock is an instant deadlock.Virtual threads
A landmark feature in Java 21: virtual threads are ultra-lightweight threads managed by the JVM, not the OS. You can have millions of them. This means you can write simple, blocking, thread-per-request code that scales like complex async code — the best of both worlds. Create them with Executors.newVirtualThreadPerTaskExecutor().
Key idea
Recap & quick check
Key takeaways
- Use an ExecutorService thread pool instead of creating threads by hand (and shut it down).
- Callable returns a value via a Future; future.get() blocks until it's ready.
- CompletableFuture chains non-blocking async steps with thenApply/thenCompose/etc.
- Use concurrent collections (ConcurrentHashMap) for shared data — never a plain HashMap.
- Locks, semaphores, and latches coordinate threads; virtual threads (Java 21) scale to millions.
Quick check
1. Why use a thread pool (ExecutorService)?
2. What does a Future represent?
3. How is CompletableFuture better than a plain Future?
4. Which map is safe for concurrent use by many threads?
5. What's special about virtual threads (Java 21)?
Superb — you can now do concurrent work the professional way. Next up: Module 23 — Database Programming with JDBC.