Chapter 1 Introduction
I Fundamentals
Chapter 2 Thread Safety
Chapter 3 Sharing Objects
Chapter 4 Composing Objects
Chapter 5 Building blocks
Learning java, javascript, HTML, CSS, shell, python with fun javascript games and videos
We have explored how structured concurrency makes multithreaded code safer and easier to manage. Now we look at a new primitive in the Java concurrency toolbox: ScopedValue
. This feature replaces many use cases of ThreadLocal
with a model better suited for virtual threads.
ScopedValue
is a safe, immutable, and inheritable thread context designed to replace ThreadLocal
in virtual-thread environments. Unlike ThreadLocal
, which relies on mutable state tied to the thread, ScopedValue
is a single-assignment object bound to a well-defined scope.
You define a value in a scope, and all code within that scope — including tasks running in virtual threads — can access it.
ThreadLocal
has long been used for per-thread data like user sessions, log context, and request-scoped state. But in the virtual thread world, it poses problems:
ScopedValue
solves these issues by being immutable and scope-bound.
static final ScopedValue<String> USER = ScopedValue.newInstance();
void handleRequest(String userId) {
ScopedValue.where(USER, userId).run(() -> {
logRequest(); // prints: user=alice
processRequest(); // can access USER.get()
});
}
Inside the ScopedValue.where(...).run()
block, the value is safely accessible. Outside, it throws IllegalStateException
if you try to read it.
Feature | ThreadLocal | ScopedValue |
---|---|---|
Mutable | Yes | No |
Garbage-safety | Manual cleanup needed | Auto scoped |
Virtual thread friendly | No | Yes |
Default value support | Yes | No (must be explicitly set) |
Inheritance | Yes (but buggy) | Explicit |
ScopedValue
works naturally with StructuredTaskScope
. For example:
ScopedValue.where(CTX, "alice").run(() -> {
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
scope.fork(() -> logContext()); // "alice"
scope.fork(() -> audit()); // "alice"
scope.join();
}
});
All subtasks share the same value safely without mutable thread-local state.
ScopedValue
for all new context-passing codeThreadLocal
in virtual thread-based systemsScopedValue.get()
only inside ScopedValue.where(...)
blocks
ScopedValue
is a modern replacement for ThreadLocal
that aligns with virtual threads, structured concurrency, and clean context propagation. It brings safety and immutability to multithreaded environments and avoids common pitfalls of legacy thread-local storage.
We explored how virtual threads enable lightweight, high-scale concurrency with traditional blocking logic. Now, we introduce structured concurrency, a powerful model that brings clarity, safety, and predictability to multithreaded programming in Java.
Structured concurrency is a programming model that treats multiple concurrent tasks running in a method as part of a single unit of work. All child threads must complete (or be canceled) before the parent scope exits. This ensures threads are well-scoped, making the code easier to reason about, debug, and manage.
Think of it as structured control flow for threads — just like try-with-resources
manages resource lifecycles, structured concurrency manages the lifecycle of spawned threads.
StructuredTaskScope
Java 21 introduces StructuredTaskScope
in java.util.concurrent
. It provides a way to spawn concurrent subtasks, wait for them to complete, and cancel others on success or failure.
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
Future<String> user = scope.fork(() -> fetchUser());
Future<String> order = scope.fork(() -> fetchOrder());
scope.join(); // Wait for all subtasks
scope.throwIfFailed(); // Propagate exceptions
return user.result() + " - " + order.result();
}
This model ensures both fetchUser()
and fetchOrder()
complete within the method’s scope. If one fails, the other is canceled automatically.
Java provides three built-in variants:
StructuredTaskScope.ShutdownOnSuccess
— cancels remaining tasks when any subtask succeedsStructuredTaskScope.ShutdownOnFailure
— cancels all tasks when one fails (for fault tolerance)StructuredTaskScope
(manual join, no auto shutdown)These offer granular control over what should happen when subtasks succeed or fail.
StructuredTaskScope
for request-scoped parallelismShutdownOnFailure
for fail-fast logic
Structured concurrency is one of the cleanest additions to Java’s concurrent toolkit in years. Next, we’ll explore ScopedValue
, Java's alternative to thread-local variables, optimized for virtual threads and structured concurrency.
With the arrival of virtual threads in Java 21, concurrent programming in Java has taken a major leap forward. Virtual threads are lightweight, memory-efficient threads managed by the JVM rather than the OS, allowing millions of concurrent tasks to run efficiently. This post explains how Java supports virtual threads, how they interact with the Java memory model, and how to modernize your existing code using Thread
, ExecutorService
, and Future
.
Virtual threads (part of Project Loom) are lightweight user-mode threads. Unlike platform threads (which map 1:1 with OS threads), virtual threads are scheduled by the JVM and can be suspended or resumed without kernel intervention.
They support blocking operations without blocking the underlying kernel thread, which dramatically simplifies concurrent applications that traditionally relied on callbacks, thread pools, or complex async APIs.
In traditional Java, every Thread
corresponds to a native OS thread. These threads are expensive to create (megabytes of memory per stack) and limited in number (~thousands max). In contrast, virtual threads are user-mode threads scheduled by the JVM, not the OS. The JVM can multiplex millions of virtual threads onto a small pool of carrier (platform) threads.
Threading Model Comparison:
Feature | Platform Thread | Virtual Thread |
---|---|---|
Mapped to OS Thread | Yes (1:1) | No (many:few) |
Thread Creation Cost | High (kernel allocation) | Low (JVM-managed) |
Scalability | Thousands | Millions |
Blocking I/O | Blocks OS thread | Unmounts, then resumes |
Stack Size | Fixed (~1MB) | Small, growable, pausable |
Virtual threads fully comply with Java’s memory model. Just like platform threads, they:
synchronized
, volatile
, and other concurrency constructsHowever, virtual threads can be unmounted (paused) when blocked and remounted on another carrier thread later. This behavior is fully transparent to developers but allows far better scalability.
new Thread()
with Thread.startVirtualThread()
// Before
new Thread(() -> {
handleRequest();
}).start();
// After
Thread.startVirtualThread(() -> {
handleRequest();
});
// Before
ExecutorService pool = Executors.newFixedThreadPool(10);
// After
ExecutorService pool = Executors.newVirtualThreadPerTaskExecutor();
With virtual threads, there’s no need to manually limit thread pool size in most applications — the JVM efficiently schedules virtual threads.
Future<Integer> result = pool.submit(() -> {
return computeValue();
});
int value = result.get(); // blocking is fine with virtual threads
Certain operations can "pin" virtual threads to carrier threads:
To avoid pinning, prefer:
java.nio
(non-blocking channels)Lock
over synchronized
if locks are held during I/O
Use jcmd
or thread dumps to verify that virtual threads are being used:
jcmd <pid> Thread.dump
Look for thread names like VirtualThread[#]
to confirm Loom is working as expected.
new Thread(...)
with Thread.startVirtualThread()
Executors.newVirtualThreadPerTaskExecutor()
CompletableFuture.supplyAsync()
Virtual threads modernize Java’s threading model, making high-concurrency programming simpler, safer, and more scalable. You can now write direct-style, blocking code that performs as well as complex async code — and converting existing thread-based applications is mostly straightforward.
Project Loom doesn't replace the need for understanding Java concurrency — but it reduces boilerplate and expands what’s possible in memory- and thread-efficient applications.