Flags and launch options
Virtual threads are standard in modern Java and usually need no JVM flag. Structured
concurrency in Java 25 uses StructuredTaskScope, which is still a preview API,
so code that uses it must be compiled and run with --enable-preview.
-Djdk.virtualThreadScheduler.parallelism sets the target number of carrier
threads used by the virtual-thread scheduler. -Djdk.virtualThreadScheduler.maxPoolSize
sets an upper bound for scheduler carrier threads when compensation is needed. -Djdk.tracePinnedThreads
is a diagnostic property for finding code that pins a virtual thread to its carrier.
-Djdk.traceVirtualThreadLocals helps find code that stores thread-local values
in virtual threads.
Option details
- --enable-preview
- Enables preview APIs at compile time and run time. In Java 25,
StructuredTaskScopeis preview, so bothjavacandjavaneed this option for code that imports it. - -Djdk.virtualThreadScheduler.parallelism=4
- Sets the target parallelism of the virtual-thread scheduler. This is the normal target number of carrier platform threads used to run virtual threads, which can be useful in small containers.
- -Djdk.virtualThreadScheduler.maxPoolSize=8
- Sets the maximum number of carrier platform threads the scheduler may create. This is different from
parallelism: parallelism is the target, while max pool size is the ceiling used when the scheduler compensates for blocking or pinned carriers. - -Djdk.tracePinnedThreads=short
- Prints a short stack trace when a virtual thread is pinned to its carrier while blocking. Use it during investigation to find synchronized or native blocking regions that limit scalability.
- -Djdk.traceVirtualThreadLocals=true
- Prints a stack trace when code sets a thread-local value from a virtual thread. Use it to find libraries or application code that accidentally keep per-request state in many short-lived virtual threads.
When to use it
Use this for services that handle many concurrent blocking I/O operations, such as HTTP calls, database queries, file operations, queue consumers, or fan-out request handlers. Virtual threads provide scale, not faster CPU execution, so they are best for waiting-heavy workloads rather than CPU-bound loops.
Structured concurrency is useful when one request splits into related subtasks and all of them must finish, fail, or be cancelled as one unit. It improves cancellation, error handling, and thread-dump observability compared with unbounded ad hoc futures.
Java version support
Virtual threads were previewed in Java 19 and Java 20, then finalized in Java 21. Structured
concurrency incubated in Java 19 and Java 20, then previewed from Java 21 through Java 25.
It continues as a sixth preview in Java 26 and a seventh preview targeted to Java 27. In
Java 25, StructuredTaskScope is still preview, so both compile and run steps need
--enable-preview.
Related JEPs
- JEP 425 - Virtual Threads preview, delivered in JDK 19.
- JEP 428 - Structured Concurrency incubator, delivered in JDK 19.
- JEP 436 - Virtual Threads second preview, delivered in JDK 20.
- JEP 437 - Structured Concurrency second incubator, delivered in JDK 20.
- JEP 444 - Virtual Threads final, delivered in JDK 21.
- JEP 453 - Structured Concurrency preview, delivered in JDK 21.
- JEP 462 - Structured Concurrency second preview, delivered in JDK 22.
- JEP 480 - Structured Concurrency third preview, delivered in JDK 23.
- JEP 491 - Synchronize Virtual Threads without Pinning, delivered in JDK 24.
- JEP 499 - Structured Concurrency fourth preview, delivered in JDK 24.
- JEP 505 - Structured Concurrency fifth preview, delivered in JDK 25.
- JEP 525 - Structured Concurrency sixth preview, targeted to JDK 26.
- JEP 533 - Structured Concurrency seventh preview, targeted to JDK 27.
Examples
Run a virtual-thread-per-task service
java -jar app.jar
No virtual-thread flag is needed when the application itself creates virtual threads.
Run Java 25 code that uses StructuredTaskScope
javac --release 25 --enable-preview Main.java
java --enable-preview Main
The preview option must be present at both compile time and run time.
Cap carrier parallelism for a constrained container
java -Djdk.virtualThreadScheduler.parallelism=4 \
-Djdk.virtualThreadScheduler.maxPoolSize=8 \
--enable-preview \
-jar app.jar
This does not limit the number of virtual threads. It sets a target of 4 carrier threads and a ceiling of 8 carriers for scheduler compensation.
Trace pinned virtual threads during investigation
java -Djdk.tracePinnedThreads=short \
--enable-preview \
-jar app.jar
Use this in staging or a focused reproduction because the trace output can be noisy.
Trace virtual-thread-local usage
java -Djdk.traceVirtualThreadLocals=true \
--enable-preview \
-jar app.jar
Use this to find thread-local writes that may create avoidable memory pressure with many virtual threads.
Complete code example
This example uses structured concurrency to fetch two pieces of a profile as one scoped operation. If one subtask fails, the scope fails as a unit instead of leaving unrelated work running in the background.
import java.time.Duration;
import java.util.concurrent.StructuredTaskScope;
import java.util.concurrent.StructuredTaskScope.Joiner;
record Profile(String user, String orders) {}
public class Main {
public static void main(String[] args) throws Exception {
System.out.println(loadProfile());
}
static Profile loadProfile() throws Exception {
try (var scope = StructuredTaskScope.open(
Joiner.allSuccessfulOrThrow(),
config -> config.withTimeout(Duration.ofSeconds(2)))) {
var user = scope.fork(Main::fetchUser);
var orders = scope.fork(Main::fetchOrders);
scope.join();
return new Profile(user.get(), orders.get());
}
}
static String fetchUser() throws InterruptedException {
Thread.sleep(Duration.ofMillis(100));
return "user-123";
}
static String fetchOrders() throws InterruptedException {
Thread.sleep(Duration.ofMillis(150));
return "orders=7";
}
}
Compile and run with --enable-preview because this example imports StructuredTaskScope.
Verify
- Measure throughput and request latency under realistic concurrent blocking I/O.
- Use thread dumps to confirm virtual threads and scope hierarchy are visible.
- Check for pinned-thread traces before assuming virtual threads will scale cleanly.
- Check virtual-thread-local traces before carrying request context with
ThreadLocal. - Keep CPU-bound work on bounded executors instead of flooding virtual threads.
References
See Oracle's Java 25 guides for virtual threads and structured concurrency.