Java Concurrency Evolution

From Virtual Threads in Java 21 to Structured Concurrency and Scoped Values. Interactive simulations showing the revolution in Java threading.

Java 21LTS

Virtual Threads

The biggest change to Java concurrency since Java 5. Virtual threads are lightweight threads managed by the JVM, not the OS. Create millions of threads with minimal memory overhead.

1M+
Concurrent Threads
vs ~10K with platform threads
~1KB
Memory per Thread
vs ~1MB for platform threads
1000x
More Efficient
For I/O-bound workloads

Creating Virtual Threads

// Before Java 21 - Platform Threads (expensive!)
ExecutorService executor = Executors.newFixedThreadPool(100);
for (int i = 0; i < 10000; i++) {
    executor.submit(() -> handleRequest());  // Limited to ~100 concurrent
}

// Java 21+ - Virtual Threads (millions possible!)
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    for (int i = 0; i < 1_000_000; i++) {
        executor.submit(() -> handleRequest());  // Each gets its own virtual thread!
    }
}

Interactive Thread Simulation

50

Platform Threads

0/50 completed
Running
Blocked (I/O)
Waiting
Completed

Virtual Threads

0/50 completed
Running
Blocked (I/O)
Waiting
Completed
Java 22-24Preview → Refinement

Structured Concurrency

Treat groups of concurrent tasks as a single unit of work. When one fails, all are cancelled. No more lost exceptions, resource leaks, or orphan threads.

Traditional (ExecutorService)

ExecutorService executor = ...
Fetch User
Fetch Orders
Fetch Recs
Future<User> user = exec.submit(
    () -> fetchUser(id));
Future<Orders> orders = exec.submit(
    () -> fetchOrders(id));
Future<Recs> recs = exec.submit(
    () -> fetchRecs(id));  // FAILS!

// Problems:
// • Other tasks keep running
// • Exception may be lost
// • Manual cleanup needed
Log

Run to see...

Structured Concurrency

StructuredTaskScope scope = ...
Fetch User
Fetch Orders
Fetch Recs
try (var scope = StructuredTaskScope
        .ShutdownOnFailure()) {
    scope.fork(() -> fetchUser(id));
    scope.fork(() -> fetchOrders(id));
    scope.fork(() -> fetchRecs(id));
    scope.join();
    scope.throwIfFailed();
}
// Benefits:
// • Auto-cancel on failure
// • Clean exception handling
// • Resources auto-cleaned
Log

Run to see...

Structured Concurrency Timeline

Java 21JEP 453PreviewInitial preview
Java 22JEP 462PreviewSecond preview, refined API
Java 23JEP 480PreviewThird preview, minor updates
Java 24JEP 499PreviewContinued refinement toward GA
Java 22-24Preview

Scoped Values

Share immutable data across threads safely. The modern replacement for ThreadLocal that works perfectly with virtual threads - no memory leaks, automatic inheritance, clear scope boundaries.

ThreadLocal (Legacy)

ThreadLocal<String> USER
Main Thread
Child Thread 1
Child Thread 2
ThreadLocal<String> USER =
    new ThreadLocal<>();

USER.set("alice@example.com");
try {
    doWork();
    // Child threads: USER.get() = null!
} finally {
    USER.remove(); // Easy to forget!
}
Log

Run to see...

ScopedValue (Java 21+)

ScopedValue<String> USER
Main Thread
Child Thread 1
Child Thread 2
ScopedValue<String> USER =
    ScopedValue.newInstance();

ScopedValue.where(USER, "alice")
    .run(() -> {
        doWork();
        // Child threads inherit USER!
    });
// Auto-unbound. No cleanup needed!
Log

Run to see...

Scoped Values Timeline

Java 20JEP 429IncubatorInitial incubator
Java 21JEP 446PreviewPromoted to preview
Java 22JEP 464PreviewSecond preview
Java 23JEP 481PreviewThird preview
Java 24JEP 487PreviewFourth preview, nearing GA
Java 23-24JEP 491

Virtual Thread Pinning Fix

In Java 21-22, virtual threads got "pinned" to carrier threads when entering synchronized blocks, preventing other virtual threads from using that carrier. Java 23-24 fixes this critical limitation.

Java 21-22 (Pinning Issue)

Run demo to see...
// Java 21-22: synchronized causes pinning
synchronized (lock) {
    // Virtual thread PINNED to carrier!
    // Carrier cannot run other VTs
    blockingIO();  // Carrier blocked too
}
// Other virtual threads starve
Log

Run to see...

Java 23-24 (Pinning Fixed)

Run demo to see...
// Java 23-24: synchronized no longer pins
synchronized (lock) {
    // Virtual thread NOT pinned!
    // Can unmount during I/O
    blockingIO();  // Carrier freed
}
// Other virtual threads can run
Log

Run to see...

What is Pinning?

Pinning occurs when a virtual thread cannot unmount from its carrier thread:

  • Java 21-22: synchronized blocks pin the virtual thread
  • Native methods (JNI) still cause pinning
  • Java 23-24: synchronized no longer pins!
Detect pinning with JFR:
// Run with JFR to detect pinning events
java -XX:StartFlightRecording=filename=rec.jfr \
     -Djdk.tracePinnedThreads=full \
     MyApp.java

// Look for jdk.VirtualThreadPinned events
Java 24New Tools

Observability & Monitoring

As Virtual Threads became the standard, Java 24 added better tools to monitor and debug them. New jcmd commands and MXBeans provide visibility into the virtual thread scheduler.

New jcmd Commands

Thread.vthread_scheduler
$ jcmd <pid> Thread.vthread_scheduler

ForkJoinPool-1-worker-1: WAITING
ForkJoinPool-1-worker-2: RUNNING vthread[#21]
ForkJoinPool-1-worker-3: PARKED
...
Virtual threads: 1,234,567 active
Thread.vthread_pollers
$ jcmd <pid> Thread.vthread_pollers

Read poller: 42 threads waiting
Write poller: 18 threads waiting
Connect poller: 5 threads waiting

VirtualThreadSchedulerMXBean

// Programmatic monitoring
MBeanServer mbs = ManagementFactory
    .getPlatformMBeanServer();

ObjectName name = new ObjectName(
    "java.lang:type=VirtualThreadScheduler");

// Get scheduler metrics
long mounted = (Long) mbs.getAttribute(
    name, "MountedVirtualThreadCount");
long queued = (Long) mbs.getAttribute(
    name, "QueuedVirtualThreadCount");
int parallelism = (Integer) mbs.getAttribute(
    name, "Parallelism");

System.out.printf(
    "Mounted: %d, Queued: %d, Workers: %d%n",
    mounted, queued, parallelism);

Available Metrics

MountedVirtualThreadCount
Virtual threads currently running on carriers
QueuedVirtualThreadCount
Virtual threads waiting to be scheduled
Parallelism
Number of carrier threads (ForkJoinPool workers)
PoolSize
Current size of the carrier thread pool
StealCount
Work-stealing events for load balancing
TotalStartedVirtualThreads
Total virtual threads started since JVM launch

Putting It All Together

Virtual Threads + Structured Concurrency + Scoped Values = The future of Java concurrency. Here's how they work together:

// The Future of Java Concurrency (Java 24+)

// 1. Define scoped values for request context
static final ScopedValue<User> CURRENT_USER = ScopedValue.newInstance();
static final ScopedValue<RequestId> REQUEST_ID = ScopedValue.newInstance();

void handleRequest(Request req) {
    User user = authenticate(req);
    RequestId id = RequestId.generate();

    // 2. Bind scoped values for this request
    ScopedValue.where(CURRENT_USER, user)
               .where(REQUEST_ID, id)
               .run(() -> {

        // 3. Use structured concurrency for parallel work
        try (var scope = StructuredTaskScope.ShutdownOnFailure()) {

            // 4. All forked tasks are virtual threads
            //    They automatically inherit scoped values!
            Subtask<Profile> profile = scope.fork(() ->
                fetchProfile(CURRENT_USER.get()));  // Has access!

            Subtask<List<Order>> orders = scope.fork(() ->
                fetchOrders(CURRENT_USER.get()));   // Has access!

            Subtask<List<Recommendation>> recs = scope.fork(() ->
                fetchRecommendations());            // Has access!

            scope.join();
            scope.throwIfFailed();

            // 5. All succeeded, return response
            return new Response(profile.get(), orders.get(), recs.get());
        }
        // Scope closes: all threads cleaned up, values unbound
    });
}

// Benefits:
// ✓ Millions of concurrent requests (Virtual Threads)
// ✓ Clean error handling (Structured Concurrency)
// ✓ Safe context propagation (Scoped Values)
// ✓ No memory leaks, no orphan threads, no lost exceptions

Concurrency Evolution: Java 21 → 24

21

Virtual Threads

Millions of lightweight threads

22

Structured Concurrency

Tasks as a unit of work

23

Scoped Values

Safe context propagation

23

Pinning Fixed

synchronized no longer pins

24

Observability

Monitor virtual threads

Demo Showcase

Auto-playing demonstration of all Java concurrency features

1 / 4

Virtual Threads

Java 21 - Platform vs Virtual Thread Comparison

Platform Threads

0/4 completed
Running
Blocked (I/O)
Waiting
Completed

Virtual Threads

0/4 completed
Running
Blocked (I/O)
Waiting
Completed