Concurrency in Java
100a1a⁝ Books - Java Concurrency
Java threading changed a lot after Java 8.
This note is my short map.
1. Old style threading
Old model:
- Use
ThreadandRunnable. - Use
ExecutorServiceand fixed thread pools. - Threads are heavy.
You cannot create many of them. - You must think a lot about:
- pool size
- blocking IO
- deadlocks and starvation
2. CompletableFuture (Java 8)
CompletableFuture was a big step forward.
- It gives async tasks with a clear API.
- You can chain steps:
thenApply,thenCompose,handle,exceptionally. - It works well for:
- calling many services in parallel
- combining results
- handling errors in one place
I still need CompletableFuture for some cases, especially for complex pipelines.
3. Flow API and reactive style (Java 9)
Java 9 added java.util.concurrent.Flow.
- It defines Publisher / Subscriber / Subscription / Processor.
- It connects Java with libraries like Reactor and RxJava.
- It is good for streaming data with backpressure.
For most normal business code I do not start with Flow, but it is part of the ecosystem.
4. Virtual threads (Project Loom, Java 21)
Virtual threads are the real revolution.
- Virtual threads are very light.
You can create thousands or even millions of them. (OS threads are expensive (2MB memory stack usually), so we couldn’t spawn 100_000 of them.) - They use normal blocking style code, but scale better.
- You can write:
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
executor.submit(() -> callRemoteService());
}Key idea: blocking is now OK again, because only the virtual thread blocks, not the OS thread.
This makes life easier:
- I can keep a simple “thread-per-request” model.
- I do not need complex reactive code for many use cases.
- Old blocking libraries (JDBC, classic HTTP clients) still work.
Caveats:
- . Don’t use for CPU-Intensive Tasks (no benefit) (why: thread never “blocks” (waits), it never yields the CPU)
- There are specific situations where a Virtual Thread gets “pinned” to the OS thread, meaning it cannot be unmounted even if it blocks. (Pinning)
- Just because you can create 1 million threads doesn’t mean your database can handle 1 million concurrent connections. (Resource Throttling)
5. Structured concurrency
StructuredTaskScope gives a clean way to run tasks in parallel as one unit.
Example use case:
- Load User
- Load Orders
- Do it in parallel
- Fail all if one fails
Pseudocode:
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
var userTask = scope.fork(() -> loadUser(id));
var ordersTask = scope.fork(() -> loadOrders(id));
scope.join();
scope.throwIfFailed();
return new UserProfile(
userTask.get(),
ordersTask.get()
);
}Benefits:
- All child tasks live inside a clear block.
- When one task fails, others stop.
- It is easier to reason about errors and cancellation.
6. New default mental model
Old model:
- Threads are expensive.
- Avoid blocking.
- Use complex thread pools and reactive frameworks.
New model with virtual threads:
- Threads are cheap.
- Blocking is fine on virtual threads.
- Start with:
- simple blocking code
- virtual threads
- structured concurrency for groups of tasks
- use
CompletableFutureonly when async composition really helps
7. What I want to use in my systems
For my own work:
- Use virtual threads as default for blocking IO.
- Use
StructuredTaskScopefor parallel calls with clear lifetime. - Use
CompletableFuturefor advanced composition cases. - Avoid low-level
Thread,wait/notify, and manual pool tuning when not needed.