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 Thread and Runnable.
  • Use ExecutorService and 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 CompletableFuture only 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 StructuredTaskScope for parallel calls with clear lifetime.
  • Use CompletableFuture for advanced composition cases.
  • Avoid low-level Thread, wait/notify, and manual pool tuning when not needed.

100a⁝ Java