Phase 4 · Advanced JavaModule 22~48 min read

Advanced Concurrency

Scale concurrency with executors, futures, locks, concurrent collections, and virtual threads.

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 Callable and Future
  • 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:

How an executor works

Task queue

T1 · T2 · T3 · T4…

→

Thread pool

N reusable threads

→

Results

Future<T>

Executor.java
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 ones
Order and thread names vary between runs.

Watch out

Always 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:

Future.java
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:

Completable.java
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 chain

Note

Combine futures with 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:

Concurrent.java
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 with tryLock, timeouts, and fairness
  • ReadWriteLock — many readers or one writer; great for read-heavy data
  • Semaphore — limit how many threads access a resource at once
  • CountDownLatch — wait until N tasks have finished
  • CyclicBarrier — make threads wait for each other at a rendezvous point

Tip

With a 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

Virtual threads make the classic "one thread per task" model viable again at massive scale — hugely relevant for servers. Paired with structured concurrency, they make concurrent code dramatically easier to write correctly.

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.