Share
Virtual Threads, Structured Concurrency et Scoped Values en Java 25 et au-delà
Le projet Loom veut simplifier le modèle de programmation des systèmes asynchrones. Le moyen est de permettre le blocage du thread qui réalise les entrées / sorties sans dégrader les performances de l'application. Cela repose sur l'utilisation de threads virtuels, que Loom nous a apporté en Java 21. Alors que les threads Java classiques sont des enveloppes sur les threads de l'OS, les threads virtuels sont de simples objets Java, que l'on peut créer à la demande. Ils posaient toutefois des problèmes de performances en 21 lors de l'exécution d'un appel I/O dans un bloc synchronisé, point qui a été résolu en Java 24. Outre les threads virtuels, Java 21 nous a apporté deux fonctionnalités en preview : les Scoped Values, qui visent à remplacer l'ancienne API ThreadLocal, et l'API Structured Concurrency, qui permet d'organiser les tâches asynchrones d'une application. L'API Scope Value est finale en Java 25. L'API Structured Concurrency quant à elle a été remaniée en 25 et est toujours en preview en 26. Nous présenterons ces trois éléments en détails, avec des exemples et des patterns de code, et aborderons ce que l'on sait des évolutions futures du projet Loom.
José
Transcript
183 slides
-
Loom in the JDK 25-26-27 Virtual Threads, Structured Concurrency, and
Scoped Values José Paumard Java Developer Advocate Java Platform Group Rémi Forax Maître de conferences Université Gustave Eiffel -
https://twitter.com/Nope! https://github.com/forax https://speakerdeck.com/forax OpenJDK, ASM, Tatoo, Pro, etc… One of
the Father of invokedynamic (Java 7) Lambda (Java 8), Module (Java 9) Constant dynamic (Java 11) Record, text blocks, sealed types (Java 14 / 15) Valhalla (Java 28+) -
Tune in! 8 Copyright © 2026, Oracle and/or its affiliates
Inside Java Newscast JEP Café Road To 25 series Inside.java Inside Java Podcast Sip of Java Cracking the Java coding interview -
OpenJDK is the place where it all happens 9 Copyright
© 2026, Oracle and/or its affiliates https://openjdk.org/ -
OpenJDK is the place where it all happens 10 Copyright
© 2026, Oracle and/or its affiliates https://jdk.java.net/ -
First Preview in the JDK 19 13 Copyright © 2026,
Oracle and/or its affiliates -
Virtual Threads: Final in 21 With one issue left to
solve: pinning on synchronization 14 Copyright © 2026, Oracle and/or its affiliates -
Scoped Value: Final in 25 15 Copyright © 2026, Oracle
and/or its affiliates -
Structured Concurrency: Preview in 26 16 Copyright © 2026, Oracle
and/or its affiliates -
… and in 27! 17 Copyright © 2026, Oracle and/or
its affiliates -
What is Loom About? Loom is about fixing concurrency issues
18 Copyright © 2026, Oracle and/or its affiliates -
What is Loom About? Why do you need concurrency? For
a given latency, the throughput of an application is limited by the number of threads The number of threads is limited by the hardware to a few thousands But the hardware can serve much more requests than that! 19 Copyright © 2026, Oracle and/or its affiliates -
What is Loom About? The first solution is to split
each request into atomic, non-blockings tasks And to send them to a reactive framework that will call them when needed But the reactive approach leads to a code that is hard to write and to read, and that is not observable 20 Copyright © 2026, Oracle and/or its affiliates -
What is Loom About? A second solution is to increase
the number of threads, which is not possible with platform threads Virtual threads have been created to be light enough so that you can increase your application throughput for a given latency 21 Copyright © 2026, Oracle and/or its affiliates -
A Simple Example Reading images and links Too unefficient to
be used 24 Copyright © 2026, Oracle and/or its affiliates var image = someService.readImage(); var links = someService.readLinks(); var page = new Page(image, links); -
Concurrency Issues The Java 5 (2004) solution (with lambdas and
var) What is wrong with this code? 25 Copyright © 2026, Oracle and/or its affiliates ExecutorService es = ...; var f1 = es.submit(someService::readImages); var f2 = es.submit(someService::readLinks); var page = new Page(f1.get(1, TimeUnit.SECONDS), f2.get(1, TimeUnit.SECONDS)); -
Concurrency Issues 1) It blocks threads Blocking a platform thread
is wrong, for many reasons (Here it is blocked for ~100ms) 26 Copyright © 2026, Oracle and/or its affiliates -> worker thread is blocked -> worker thread is blocked -> the main thread is blocked ExecutorService es = ...; var f1 = es.submit(someService::readImages); var f2 = es.submit(someService::readLinks); var page = new Page(f1.get(1, TimeUnit.SECONDS), f2.get(1, TimeUnit.SECONDS)); -
Concurrency Issues 2) It can lead to a loose thread
27 Copyright © 2026, Oracle and/or its affiliates ExecutorService es = ...; var f1 = es.submit(someService::readImages); var f2 = es.submit(someService::readLinks); var page = new Page(f1.get(1, TimeUnit.SECONDS), f2.get(1, TimeUnit.SECONDS)); -
Concurrency Issues 2) It can lead to a loose thread
28 Copyright © 2026, Oracle and/or its affiliates ExecutorService es = ...; var f1 = es.submit(someService::readImages); var f2 = es.submit(someService::readLinks); var page = new Page(f1.get(1, TimeUnit.SECONDS), f2.get(1, TimeUnit.SECONDS)); -> if f1.get() crashes... -
Concurrency Issues 2) It can lead to a loose thread
If something goes wrong in readLinks, then the thread running it is never freed. You now have a loose thread! 29 Copyright © 2026, Oracle and/or its affiliates ExecutorService es = ...; var f1 = es.submit(someService::readImages); var f2 = es.submit(someService::readLinks); var page = new Page(f1.get(1, TimeUnit.SECONDS), f2.get(1, TimeUnit.SECONDS)); -> if f1.get() crashes... ... f2.get() is never called -
Concurrency Issues 3) It’s hard to debug Stack traces do
not give you the information you need. 30 Copyright © 2026, Oracle and/or its affiliates ExecutorService es = ...; var f1 = es.submit(someService::readImages); var f2 = es.submit(someService::readLinks); var page = new Page(f1.get(1, TimeUnit.SECONDS), f2.get(1, TimeUnit.SECONDS)); -
Concurrency Issues 3 issues to fix: - Blocking a platform
thread is bad - Having loose threads is a waste of resources - Having a non-relevant stack trace is annoying 31 Copyright © 2026, Oracle and/or its affiliates -
What is a Platform Thread? A java.lang.Thread is a wrapper
on a kernel (or platform) thread It needs: - ~1ms to start - ~2MB of memory to store its stack - context switching costs ~0,1ms You can only have several thousands of them 32 Copyright © 2026, Oracle and/or its affiliates -
Why is it Wrong to Block a P. Thread? Handling
one network request per thread Limits your application to ~10k concurrent requests If handling one request consumes 1s of CPU time Then your CPU can handle 1M requests per second The model one request per thread does not work with Platform Threads (anymore) 33 Copyright © 2026, Oracle and/or its affiliates -
What Solution? A first solution: - The reactive programming solution:
have a platform thread handle many requests - How many? ~1k per platform thread 34 Copyright © 2026, Oracle and/or its affiliates -
Let’s Rewrite the Previous Code This is the classical way
of writing it 35 Copyright © 2026, Oracle and/or its affiliates ExecutorService es = ...; var f1 = es.submit(someService::readImages); var f2 = es.submit(someService::readLinks); var page = new Page(f1.get(1, TimeUnit.SECONDS), f2.get(1, TimeUnit.SECONDS)); -
Let’s Rewrite the Previous Code This is the CompletableFuture way
of writing it 36 Copyright © 2026, Oracle and/or its affiliates var cf1 = CompletableFuture .supplyAsync(someService::readImages) .whenComplete((images, error) -> { if (error != null) { // log something } }); var cf2 = // same with readLinks var cf3 = cf1.runAfterBoth(cf2, () -> { var page = new Page(cf1.get(), cf2.get()); // do something with page }); -
How Is Your Platform Thread Used? 37 Copyright © 2026,
Oracle and/or its affiliates Platform Thread -
How Is Your Platform Thread Used? One thing: never write
blocking code in your lambdas! 38 Copyright © 2026, Oracle and/or its affiliates ~100ns Platform Thread -
How Is Your Platform Thread Used? One thing: never write
blocking code in your lambdas! Blocking a single task for 100ms will block this thread for 1 million tasks 39 Copyright © 2026, Oracle and/or its affiliates ~100ns Platform Thread -
What About Debug? Suppose something wrong happens in readImages() Your
stack trace tells you who called readImages() 40 Copyright © 2026, Oracle and/or its affiliates 27: var images = someService.readImages(); Exception in thread "main" java.lang.IllegalStateException: Boom! at org.myapp.SomeService.readImages(SomeService.java:12) at org.myapp.MyApp.main(MyApp.java:27) -
What About Debug? Suppose something wrong happens in readImages() 41
Copyright © 2026, Oracle and/or its affiliates Exception in thread "main" java.util.concurrent.ExecutionException: java.lang.IllegalStateException: Boom! at java.base/java.util.concurrent.FutureTask.report(FutureTask.java:122) at java.base/java.util.concurrent.FutureTask.get(FutureTask.java:191) at org.myapp.MyApp.main(MyApp.java:32) Caused by: java.lang.IllegalStateException: Boom! at org.myapp.SomeService.readImages(SomeService.java:12) at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:317) at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144) at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642) at java.base/java.lang.Thread.run(Thread.java:1575) 29: var f1 = es.submit(someService::readImages); 30: var f2 = es.submit(someService::readLinks); 31: 32: var p = new Page(f1.get(), f2.get()); -
What About Debug? Suppose something wrong happens in readImages() 42
Copyright © 2026, Oracle and/or its affiliates var cf1 = CompletableFuture.supplyAsync(SomeService::readImages) .whenComplete((images, error) -> { if (error != null) { error.printStackTrace(); error.getCause().printStackTrace(); } }); var cf2 = // the same with readLinks cf1.runAfterBoth(cf2, () -> { try { var page = new Page(cf1.get(), cf2.get()); // do something with page } catch (Exception e) { // do something with the error } }); -
What About Debug? Suppose something wrong happens in readImages() 43
Copyright © 2026, Oracle and/or its affiliates java.util.concurrent.CompletionException: java.lang.IllegalStateException: Boom! at java.base/java.util.concurrent.CompletableFuture.wrapInCompletionException(CompletableFuture.java:323) at java.base/java.util.concurrent.CompletableFuture.encodeThrowable(CompletableFuture.java:359) at java.base/java.util.concurrent.CompletableFuture.completeThrowable(CompletableFuture.java:364) at java.base/java.util.concurrent.CompletableFuture$AsyncSupply.run(CompletableFuture.java:1814) at java.base/java.util.concurrent.CompletableFuture$AsyncSupply.exec(CompletableFuture.java:1804) at java.base/java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:507) at java.base/java.util.concurrent.ForkJoinPool$WorkQueue.topLevelExec(ForkJoinPool.java:1458) at java.base/java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:2034) at java.base/java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:189) var cf1 = CompletableFuture.supplyAsync(SomeService::readImages) .whenComplete((images, error) -> { if (error != null) { error.printStackTrace(); error.getCause().printStackTrace(); } }); -
What About Debug? Suppose something wrong happens in readImages() 44
Copyright © 2026, Oracle and/or its affiliates var cf1 = CompletableFuture.supplyAsync(SomeService::readImages) .whenComplete((images, error) -> { if (error != null) { error.printStackTrace(); error.getCause().printStackTrace(); } }); java.lang.IllegalStateException: Boom! at org.paumard.SomeService.readImages(SomeService.java:12) at java.base/java.util.concurrent.CompletableFuture$AsyncSupply.run(CompletableFuture.java:1812) at java.base/java.util.concurrent.CompletableFuture$AsyncSupply.exec(CompletableFuture.java:1804) at java.base/java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:507) at java.base/java.util.concurrent.ForkJoinPool$WorkQueue.topLevelExec(ForkJoinPool.java:1458) at java.base/java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:2034) at java.base/java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:189) -
Async Programming - Efficient - The programming model is hard
to read / write / test - Impossible to debug - Impossible to profile - Don’t write blocking lambdas! 45 Copyright © 2026, Oracle and/or its affiliates -
What is the Second Solution? Two solutions: - The reactive
programming solution: have a platform thread handle many requests - Build a model of thread lighter than a Platform Thread - How many? ~1k per Platform Thread - How much lighter? At least~1k lighter 46 Copyright © 2026, Oracle and/or its affiliates -
Being Light Is Not Enough! You also must: - maintain
similar performance (same latency) - offer a simple programming model - enable debugging capabilities - support profiling 47 Copyright © 2026, Oracle and/or its affiliates -
How Are Virtual Threads Working? Virtual Threads are sent to
a Fork / Join pool 48 Copyright © 2026, Oracle and/or its affiliates heap Worker 1 fork join pool Worker 2 Worker 3 Worker 4 -
How Are Virtual Threads Working? Virtual Threads are sent to
a Fork / Join pool 49 Copyright © 2026, Oracle and/or its affiliates heap Worker 1 fork join pool Worker 2 Worker 3 Worker 4 -
How Are Virtual Threads Working? … and executed by the
workers 50 Copyright © 2026, Oracle and/or its affiliates heap Worker 1 fork join pool Worker 2 Worker 3 Worker 4 -
How Are Virtual Threads Working? … and executed by the
workers 51 Copyright © 2026, Oracle and/or its affiliates heap Worker 1 fork join pool Worker 2 Worker 3 Worker 4 -
How Are Virtual Threads Working? … if it issues a
blocking call 52 Copyright © 2026, Oracle and/or its affiliates heap Worker 1 fork join pool Worker 2 Worker 3 Worker 4 -
How Are Virtual Threads Working? … if it issues a
blocking call 53 Copyright © 2026, Oracle and/or its affiliates heap Worker 1 fork join pool Worker 2 Worker 3 Worker 4 blocked! -
How Are Virtual Threads Working? … it is unmounted from
its worker 54 Copyright © 2026, Oracle and/or its affiliates heap Worker 1 fork join pool Worker 2 Worker 3 Worker 4 blocked! -
How Are Virtual Threads Working? … and its stack is
moved to the heap 55 Copyright © 2026, Oracle and/or its affiliates heap Worker 1 fork join pool Worker 2 Worker 3 Worker 4 -
How Are Virtual Threads Working? … and its stack is
moved to the heap 56 Copyright © 2026, Oracle and/or its affiliates heap Worker 1 fork join pool Worker 2 Worker 3 Worker 4 -
How Are Virtual Threads Working? …until the OS signals that
the data is available 57 Copyright © 2026, Oracle and/or its affiliates heap Worker 1 fork join pool Worker 2 Worker 3 Worker 4 -
How Are Virtual Threads Working? … at which point it
is moved back to its wait list 58 Copyright © 2026, Oracle and/or its affiliates heap Worker 1 fork join pool Worker 2 Worker 3 Worker 4 -
How Are Virtual Threads Working? … at which point it
is moved back to its wait list 59 Copyright © 2026, Oracle and/or its affiliates heap Worker 1 fork join pool Worker 2 Worker 3 Worker 4 -
How Are Virtual Threads Working? … where it can be
stolen from another available worker 60 Copyright © 2026, Oracle and/or its affiliates heap Worker 1 fork join pool Worker 2 Worker 3 Worker 4 -
How Are Virtual Threads Working? … where it can be
stolen from another available worker 61 Copyright © 2026, Oracle and/or its affiliates heap Worker 1 fork join pool Worker 2 Worker 3 Worker 4 -
What Could Prevent Moving a VT Stack? 1) Synchronized blocks:
fixed in JDK 24 A blocking call from a synchronized block pins the VT 2) Any Java code calling some native code prevents the copy of the stack to the heap. Ex: native calls using FFM, Netty native implementations And since the address is in the C code, it cannot be fixed 62 Copyright © 2026, Oracle and/or its affiliates -
Synchronized Before Java 24 Synchronized uses several memory regions as
lock - If only one thread grabs the lock - The lock is stored on the stack - If another thread tries to grab the lock - The lock is inflated to a fat lock - Allocated / deallocated on the C heap 63 Copyright © 2026, Oracle and/or its affiliates -
FastLocking 64 Copyright © 2026, Oracle and/or its affiliates void
main() { var page = buildPage(); } Page buildPage() { var links = readLinks(); ... return page; } Links readLinks() { synchronized (object) { ... } return links; } Stack buildPage readLinks main -
FastLocking Before entering a synchronized block 65 Copyright © 2026,
Oracle and/or its affiliates Links readLinks() { synchronized (object) { ... } return links; } GC Heap Stack buildPage readLinks main lock bit: 01 field 1 field 2 ... -
FastLocking "Allocate" the lock on stack 66 Copyright © 2026,
Oracle and/or its affiliates Links readLinks() { synchronized (object) { ... } return links; } GC Heap Stack buildPage readLinks main address-00 field 1 field 2 ... -
Problem with Virtual Threads There is an inner pointer to
the stack The stack cannot be copied! 67 Copyright © 2026, Oracle and/or its affiliates Links readLinks() { synchronized (object) { IO.call(); } return links; } GC Heap Stack buildPage readLinks main address-00 field 1 field 2 ... -
FastLocking with Liliput Before entering a synchronized block 68 Copyright
© 2026, Oracle and/or its affiliates Links readLinks() { synchronized (object) { ... } return links; } GC Heap Stack buildPage readLinks main lock bit: 00 field 1 field 2 ... lock stack -
FastLocking with Liliput "Allocate" the lock at the bottom of
the stack 69 Copyright © 2026, Oracle and/or its affiliates Links readLinks() { synchronized (object) { ... } return links; } GC Heap Stack buildPage readLinks main lock bit: 01 field 1 field 2 ... lock stack -
No Problem with Virtual Threads There is no inner pointer
to the stack anymore! 70 Copyright © 2026, Oracle and/or its affiliates Links readLinks() { synchronized (object) { IO.call(); } return links; } GC Heap Stack buildPage readLinks main lock bit: 01 field 1 field 2 ... lock stack -
Pinning a VT on a Platform Thread Possible address on
the stack leads to pinning 71 Copyright © 2026, Oracle and/or its affiliates Links readLinks() { someNativeCall(); } Stack native call Java code int someNativeCall() { int count; int* p = &count; // address on the stack blockingCall(); } -
Pinning a VT on a Platform Thread 72 Copyright ©
2026, Oracle and/or its affiliates Links readLinks() { someNativeCall(); } Stack int someNativeCall() { int count; int* p = &count; // address on the stack someJavaUpCall(); } Java code void javaUpCall() { someBlockingCall(); // pinned } native call Java code -
Pinning a VT on a Platform Thread Pinning on synchronization:
fixed in JDK 24 ☺ 73 Copyright © 2026, Oracle and/or its affiliates Links readLinks() { synchronized(key) { blockingCall(); // pinned up to JDK 24 } } Stack Java code -
At this Point: A virtual thread is a regular Java
object Blocking a virtual thread does not block its carrier thread Virtual threads should only be used for blocking tasks Don’t use virtual threads for long running in-memory computations! 74 Copyright © 2026, Oracle and/or its affiliates -
How Is Your Platform Thread Used? So what is the
difference with the reactive model? 75 Copyright © 2026, Oracle and/or its affiliates Platform Thread -
How Is Your Platform Thread Used? Virtual Threads are not
about performance! They are about programming model and observability 76 Copyright © 2026, Oracle and/or its affiliates Platform Thread -
Virtual Threads Programming Model The code you write is the
most basic code you can write Virtual threads handle the blocking API for you Writing blocking code is OK (Actually, this is what you should do) 77 Copyright © 2026, Oracle and/or its affiliates var images = someService.readImages(); -
Virtual Threads Programming Model And if something goes wrong: 78
Copyright © 2026, Oracle and/or its affiliates 78: Callable<Images> fetchImages = () -> someService.readImages(); 79: var future = Executors.newVirtualThreadPerTaskExecutor() 80: .submit(fetchImages); 81: IO.println(future.get()); Exception in thread "main" java.util.concurrent.ExecutionException: java.lang.IllegalStateException: Boom! at java.base/java.util.concurrent.FutureTask.report(FutureTask.java:122) at java.base/java.util.concurrent.FutureTask.get(FutureTask.java:191) at org.paumard.MyApp.main(MyApp.java:81) Caused by: java.lang.IllegalStateException: Boom! at org.paumard.SomeService.readImages(SomeService.java:12) at org.paumard.MyApp.lambda$main$0(MyApp.java:78) at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:317) at java.base/java.lang.VirtualThread.run(VirtualThread.java:329) -
Scoped Values The goal of Scoped Values is to fix
the issues ThreadLocal have 81 Copyright © 2026, Oracle and/or its affiliates -
What is Wrong with ThreadLocal? So many things… - They
are mutable - The VM cannot optimize them - They may be kept alive forever Virtual Threads support ThreadLocal variables! 82 Copyright © 2026, Oracle and/or its affiliates -
Welcome to ScopedValue! ScopedValues are non-modifiable They are not bound
to a particular thread They are bound to a single method call 83 Copyright © 2026, Oracle and/or its affiliates static final ScopedValue<String> KEY = ScopedValue.newInstance(); -
Welcome to ScopedValue! ScopedValues are non-modifiable They are not bound
to a particular thread They are bound to a single method call 84 Copyright © 2026, Oracle and/or its affiliates static final ScopedValue<String> KEY = ScopedValue.newInstance(); ... ScopedValue.where(KEY, "Value_1") .run(() -> doSomethingSmart())); -
Welcome to ScopedValue! ScopedValues are non-modifiable They are not bound
to a particular thread They are bound to a single method call 85 Copyright © 2026, Oracle and/or its affiliates static final ScopedValue<String> KEY = ScopedValue.newInstance(); ... ScopedValue.where(KEY, "Value_1") .where(OTHER_KEY, "Value_2") .run(() -> doSomethingSmart())); -
Welcome to ScopedValue! ScopedValues are non-modifiable They are not bound
to a particular thread They are bound to a single method call 86 Copyright © 2026, Oracle and/or its affiliates void doSomethingSmart() { if (KEY.isBound()) { String value = KEY.get(); ... } else { throw new IllegalStateException("Key is not bound"); } } -
How Do Scoped Values Work? Each stack has a reference
to the thread running it 87 Copyright © 2026, Oracle and/or its affiliates Stack main() -
How Do Scoped Values Work? Each thread has a reference
to an empty Snapshot A Snapshot can store bindings between keys and values 88 Copyright © 2026, Oracle and/or its affiliates Stack main() Snapshot -
How Do Scoped Values Work? The new bindings are stored
in a new snapshot Snapshots form a linked list 89 Copyright © 2026, Oracle and/or its affiliates void main() { ScopedValue .where(K1, "V1") .where(K2, "V2") .run(() -> readPage())); } Snapshot K1 V1 K2 V2 Stack main() Snapshot -
How Do Scoped Values Work? When you read a binding
You explore the linked list But reading a linked list is slow → you need a cache 90 Copyright © 2026, Oracle and/or its affiliates void readPage() { var v1 = K1.get(); } Snapshot K1 V1 K2 V2 Stack main() Snapshot readpage() -
How Do Scoped Values Work? When you read a binding
You explore the linked list But reading a linked list is slow → you need a cache 91 Copyright © 2026, Oracle and/or its affiliates void readPage() { var v1 = K1.get(); } Snapshot K1 V1 K2 V2 Stack main() Snapshot readpage() K1 -
How Do Scoped Values Work? When a binding is read
1) You check the cache 2) If it is not found then you explore the linked list 3) You populate the cache 92 Copyright © 2026, Oracle and/or its affiliates void readPage() { var v1 = K1.get(); } Snapshot K1 V1 K2 V2 Stack main() Snapshot readpage() K1 -
Snapshot K1 V1 K2 V2 How Do Scoped Values Work?
93 Copyright © 2026, Oracle and/or its affiliates void readPage() { var v1 = K1.get(); var v2 = K2.get(); } Stack main() Snapshot readpage() K2 K1 -
Snapshot K1 V1 K2 V2 How Do Scoped Values Work?
All the subsequent reads for K1 and K2 can get the value from the cache 94 Copyright © 2026, Oracle and/or its affiliates void readPage() { var v1 = K1.get(); var v2 = K2.get(); } Stack main() Snapshot readpage() K2 K1 -
How Do Scoped Values Work? When you create another binding
You remove the keys from the cache 95 Copyright © 2026, Oracle and/or its affiliates Stack main() void readPage() { var v1 = K1.get(); var v2 = K2.get(); ScopedValue .where(K1, "V10") .run(() -> readLinks())); } Snapshot Snapshot K1 V1 K2 V2 readpage() K2 -
How Do Scoped Values Work? When you create another binding
You remove the keys from the cache, and add a Snapshot 96 Copyright © 2026, Oracle and/or its affiliates Stack main() void readPage() { var v1 = K1.get(); var v2 = K2.get(); ScopedValue .where(K1, "V10") .run(() -> readLinks())); } Snapshot Snapshot K1 V1 K2 V2 readpage() K2 Snapshot K1 V10 -
How Do Scoped Values Work? The binding are ordered from
the youngest to the oldest So a rebinding always takes precedence 97 Copyright © 2026, Oracle and/or its affiliates Stack main() void readLinks() { var v1 = K1.get(); var v2 = K2.get(); } Snapshot Snapshot K1 V1 K2 V2 readPage() readLinks() Snapshot K1 V10 K2 K1 -
How Do Scoped Values Work? When returning 1) The keys
are removed 98 Copyright © 2026, Oracle and/or its affiliates void readPage() { var v1 = K1.get(); ScopedValue .where(K1, "V10") .run(() -> readLinks())); IO.println("Done"); } Stack main() Snapshot Snapshot K1 V1 K2 V2 readPage() readLinks() Snapshot K1 V10 K2 -
How Do Scoped Values Work? When returning 1) The keys
are removed 2) The Snapshot is removed 99 Copyright © 2026, Oracle and/or its affiliates Stack main() void readPage() { var v1 = K1.get(); ScopedValue .where(K1, "V10") .run(() -> readLinks())); IO.println("Done"); } Snapshot Snapshot K1 V1 K2 V2 readPage() K2 -
How Do Scoped Values Work? Subsequent reads can repopulate the
cache 100 Copyright © 2026, Oracle and/or its affiliates Stack main() void readPage() { var v1 = K1.get(); ScopedValue .where(K1, "V10") .run(() -> readLinks())); IO.println("Done"); var v2 = K1.get(); } Snapshot Snapshot K1 V1 K2 V2 readPage() K2 K1 -
How Do Scoped Values Work? In the end the cache
is empty, and all the Snapshots are removed 101 Copyright © 2026, Oracle and/or its affiliates Stack main() void main() { ScopedValue .where(K1, "V1") .where(K2, "V2") .run(() -> readPage())); IO.println("Done"); } Snapshot -
ScopedValue Scoped Values are not transmitted to threads reason: you
don’t want loose scoped values They are transmitted to StructuredTaskScope reason: you can’t have loose scoped values More on this point later 102 Copyright © 2026, Oracle and/or its affiliates -
Why GOTO are Banned? 105 Copyright © 2026, Oracle and/or
its affiliates -
Why GOTO are Banned? Yes, why? © Apple Corp. 2014
106 Copyright © 2026, Oracle and/or its affiliates -
Why GOTO are Banned? Because a GOTO can take you
anywhere in your code When a bug occurs on a given line It’s impossible to know where the program is coming from There is no stack trace when using GOTO 107 Copyright © 2026, Oracle and/or its affiliates -
Executor Running a Task in a Worker Thread 108 Copyright
© 2026, Oracle and/or its affiliates Instructions Launch a task in another thread More instructions Runnable -
Why GOTO are Banned? Running a task in another thread
Whether it is in an ExecutorService Or through a CompletableFuture Is like using a GOTO! The information on the caller is lost Structured Concurrency fixes that 109 Copyright © 2026, Oracle and/or its affiliates -
Executor Running a Task in a Worker Thread 110 Copyright
© 2026, Oracle and/or its affiliates Instructions Launch a task in another thread More instructions Runnable -
Executor What About ThreadLocal? 111 Copyright © 2026, Oracle and/or
its affiliates Instructions Launch a task in another thread More instructions Set a ThreadLocal -
Executor What About ThreadLocal? 112 Copyright © 2026, Oracle and/or
its affiliates Instructions Launch a task in another thread More instructions Set a ThreadLocal -
Executor What About ThreadLocal? 113 Copyright © 2026, Oracle and/or
its affiliates Instructions Launch a task in another thread More instructions Set a ThreadLocal If not removed… … it is also available here! -
Running Asynchronous Tasks 114 Copyright © 2026, Oracle and/or its
affiliates Instructions async tasks Executor -
Running Asynchronous Tasks 115 Copyright © 2026, Oracle and/or its
affiliates Instructions async tasks Executor -
Running Asynchronous Tasks 116 Copyright © 2026, Oracle and/or its
affiliates Instructions async tasks Executor -
Running Asynchronous Tasks 117 Copyright © 2026, Oracle and/or its
affiliates Instructions async tasks Executor -
Running Asynchronous Tasks 118 Copyright © 2026, Oracle and/or its
affiliates Instructions async tasks Executor -
Running Asynchronous Tasks 119 Copyright © 2026, Oracle and/or its
affiliates StructuredTaskScope Instructions async tasks -
Running Asynchronous Tasks 120 Copyright © 2026, Oracle and/or its
affiliates StructuredTaskScope Instructions async tasks -
Running Asynchronous Tasks 121 Copyright © 2026, Oracle and/or its
affiliates More instructions StructuredTaskScope close() Instructions async tasks scope.close() is called It interrupts the threads that are still running -
Running Asynchronous Tasks 122 Copyright © 2026, Oracle and/or its
affiliates StructuredTaskScope close() Instructions async tasks It a task runs forever, and does not react to the interruption Then close() blocks You can get a stack trace that helps you debug the situation -
Running Asynchronous Tasks 123 Copyright © 2026, Oracle and/or its
affiliates More instructions StructuredTaskScope close() Instructions async tasks No more loose thread in your application ☺ -
Executor Are Scoped Values Sent to Executors? 124 Copyright ©
2026, Oracle and/or its affiliates Instructions Bind a scoped value More instructions Runnable -
Executor Are Scoped Values Sent to Executors? 125 Copyright ©
2026, Oracle and/or its affiliates Instructions Bind a scoped value More instructions This could escape the scope of your task Runnable -
Executor Are Scoped Values Sent to Executors? 126 Copyright ©
2026, Oracle and/or its affiliates Instructions Bind a scoped value More instructions This could escape the scope of your task = possible loose binding! Runnable -
Executor Are Scoped Values Sent to Executors? 127 Copyright ©
2026, Oracle and/or its affiliates Instructions Bind a scoped value More instructions Scoped value bindings are not available here Runnable -
Bind a scoped value Are Scoped Values Sent to VTs
in STS? 128 Copyright © 2026, Oracle and/or its affiliates More instructions Instructions async tasks StructuredTaskScope No loose binding is possible -
Bind a scoped value Are Scoped Values Sent to VTs
in STS? 129 Copyright © 2026, Oracle and/or its affiliates More instructions Instructions async tasks StructuredTaskScope No loose binding is possible = Your async tasks can see the Scoped Value bindings -
How is Structured Concurrency Working? Structured Concurrency consists in creating
an object 1) That you create when needed 2) That accepts tasks (Runnable, Callable) 3) That executes them in virtual threads 4) That gives you tools to analyze the results, and deal with timeouts and exceptions 5) That is closed after use 130 Copyright © 2026, Oracle and/or its affiliates -
How is Structured Concurrency Working? Using a StructuredTaskScope: 131 Copyright
© 2026, Oracle and/or its affiliates try (var scope = StructuredTaskScope.open()) { } -
How is Structured Concurrency Working? Using a StructuredTaskScope: 132 Copyright
© 2026, Oracle and/or its affiliates try (var scope = StructuredTaskScope.open()) { var imagesSubtask = scope.fork(() -> someService.readImages()); var linksSubtask = scope.fork(() -> someService.readLinks()); } -
How is Structured Concurrency Working? Using a StructuredTaskScope: 133 Copyright
© 2026, Oracle and/or its affiliates try (var scope = StructuredTaskScope.open()) { var imagesSubtask = scope.fork(() -> someService.readImages()); var linksSubtask = scope.fork(() -> someService.readLinks()); scope.join(); // blocks until all the tasks complete } -
How is Structured Concurrency Working? Using a StructuredTaskScope: 134 Copyright
© 2026, Oracle and/or its affiliates try (var scope = StructuredTaskScope.open()) { var imagesSubtask = scope.fork(() -> someService.readImages()); var linksSubtask = scope.fork(() -> someService.readLinks()); scope.join(); var page = new Page(imagesSubtask.get(), linksSubtask.get()); // do something with page } -
How is Structured Concurrency Working? Using a StructuredTaskScope: 135 Copyright
© 2026, Oracle and/or its affiliates try (var scope = StructuredTaskScope.open()) { var imagesSubtask = scope.fork(() -> someService.readImages()); var linksSubtask = scope.fork(() -> someService.readLinks()); scope.join(); var page = new Page(imagesSubtask.get(), linksSubtask.get()); // do something with page } // scope.close() is called -
How is Structured Concurrency Working? The Subtask object UNAVAILABLE is
the default state Moves to the SUCCESS or the FAILED state 137 Copyright © 2026, Oracle and/or its affiliates sealed interface Subtask<T> { State state(); // UNAVAILABLE | SUCCESS | FAILED T get(); // get() and exception() Throwable exception(); // must be called after join() } -
How is Structured Concurrency Working? What if a callable throws
an exception? FailedException wraps this exception 138 Copyright © 2026, Oracle and/or its affiliates try (var scope = StructuredTaskScope.open()) { scope.fork(() -> { throw new HorribleException(); }); try { scope.join(); } catch (FailedException e) { ... } // ... } -
How is Structured Concurrency Working? What if a callable throws
an exception? 139 Copyright © 2026, Oracle and/or its affiliates try (var scope = StructuredTaskScope.open()) { 62: var imagesSubtask = scope.fork(() -> someService.readImages()); var linksSubtask = scope.fork(() -> someService.readLinks()); try { scope.join(); } catch (FailedException _) {} if (imagesSubtask.state() == State.FAILED) { imagesSubtask.exception().printStackTrace(); } // ... } -
How is Structured Concurrency Working? What if something goes wrong?
140 Copyright © 2026, Oracle and/or its affiliates java.lang.IllegalStateException: Boom! at org.myapp.SomeService.readImages(SomeService.java:12) at org.myapp.MyApp.lambda$main$0(MyApp.java:62) at java.base/java.util.concurrent. StructuredTaskScope$SubtaskImpl.run(StructuredTaskScope.java:893) at java.base/java.lang.VirtualThread.run(VirtualThread.java:329) -
STS Configuration Uses the Ruby builder pattern 141 Copyright ©
2026, Oracle and/or its affiliates try (var scope = StructuredTaskScope.open( joiner, // more on that in a minute configuration -> configuration.withName(...) .withThreadFactory(...) .withTimeout(...))) { // ... } -
STS Default Strategy Default behavior: join() throws an exception if
a task fails, and returns null 143 Copyright © 2026, Oracle and/or its affiliates try (var scope = StructuredTaskScope.open()) { // fork your tasks var result = scope.join(); // ... } -
Choosing a STS Strategy You can also pass a Joiner
144 Copyright © 2026, Oracle and/or its affiliates Joiner<T, R> joiner = ...; Callable<T> task = ...; try (var scope = StructuredTaskScope.open(joiner)) { // fork your tasks scope.fork(task); R result = scope.join(); // ... } -
Choosing a STS Strategy You can also pass a Joiner
(JDK 27) 145 Copyright © 2026, Oracle and/or its affiliates Joiner<T, R, R_X> joiner = ...; Callable<T> task = ...; try (var scope = StructuredTaskScope.open(joiner)) { // fork your tasks scope.fork(task); R result = scope.join(); // ... } -
The Joiner API Joiner is an interface that allows the
definition of a strategy: - How a succeeding or failing task impacts the STS - What the join() returns A Joiner can decide to cancel a STS - On the submission of a task - On the completion (success / failure) of a task 146 Copyright © 2026, Oracle and/or its affiliates -
The Joiner API The Joiner interface in 25 If onFork()
or onComplete() returns true, the scope is canceled 147 Copyright © 2026, Oracle and/or its affiliates interface Joiner<T, R> { // T: tasks, R: returned value boolean onFork(Subtask<? extends T> subtask); // called in the main thread boolean onComplete(Subtask<? extends T> subtask); // called in each VT R result() throws Throwable; // returned by join() } -
The Joiner API The Joiner interface in 26 If onFork()
or onComplete() returns true, the scope is canceled 148 Copyright © 2026, Oracle and/or its affiliates interface Joiner<T, R> { // T: tasks, R: returned value boolean onFork(Subtask<T> subtask); // called in the main thread boolean onComplete(Subtask<T> subtask); // called in each VT R result() throws Throwable; // returned by join() void onTimeout(); } -
The Joiner API in 27 The Joiner interface in 27
If onFork() or onComplete() returns true, the scope is canceled 149 Copyright © 2026, Oracle and/or its affiliates interface Joiner<T, R, R_X> { // T: tasks, R: returned value, R_X: exception boolean onFork(Subtask<T> subtask); // called in the main thread boolean onComplete(Subtask<T> subtask); // called in each VT R result() throws R_X; // returned / thrown by join() void timeout() throws R_X; } -
Open / Fork / Join / Close 150 Copyright ©
2026, Oracle and/or its affiliates 1) Create a STS: STS.open(joiner) StructuredTaskScope Main thread -
Open / Fork / Join / Close 151 Copyright ©
2026, Oracle and/or its affiliates 2) scope.fork(task) For each task: 1) Create a Subtask, state is UNAVAILABLE 2) Calls joiner.onFork(subtask) 3) Creates a VT that runs the task StructuredTaskScope Main thread -
Open / Fork / Join / Close 152 Copyright ©
2026, Oracle and/or its affiliates Subtask: UNAVAILABLE Subtask: UNAVAILABLE Subtask: UNAVAILABLE Subtask: UNAVAILABLE StructuredTaskScope 3) scope.join() (this is a blocking call) Main thread -
Open / Fork / Join / Close 153 Copyright ©
2026, Oracle and/or its affiliates StructuredTaskScope 3) scope.join() (this is a blocking call) 1) The subtask state is updated to SUCCESS Subtask: UNAVAILABLE Subtask: UNAVAILABLE Subtask: UNAVAILABLE Subtask: UNAVAILABLE Main thread -
Open / Fork / Join / Close 154 Copyright ©
2026, Oracle and/or its affiliates StructuredTaskScope 3) scope.join() (this is a blocking call) 1) The subtask state is updated to SUCCESS Subtask: UNAVAILABLE Subtask: SUCCESS Subtask: UNAVAILABLE Subtask: UNAVAILABLE Main thread -
Open / Fork / Join / Close 155 Copyright ©
2026, Oracle and/or its affiliates StructuredTaskScope 3) scope.join() (this is a blocking call) 1) The subtask state is updated to SUCCESS 2) The result is copied to the subtask 3) onComplete(subtask) is called 4) The VT dies Subtask: UNAVAILABLE Subtask: SUCCESS Subtask: UNAVAILABLE Subtask: UNAVAILABLE Main thread -
Open / Fork / Join / Close 156 Copyright ©
2026, Oracle and/or its affiliates StructuredTaskScope 3) scope.join() (this is a blocking call) Subtask: UNAVAILABLE Subtask: SUCCESS Subtask: UNAVAILABLE Subtask: UNAVAILABLE Main thread -
Open / Fork / Join / Close 157 Copyright ©
2026, Oracle and/or its affiliates StructuredTaskScope 3) scope.join() (this is a blocking call) Subtask: UNAVAILABLE Subtask: SUCCESS Subtask: UNAVAILABLE Subtask: UNAVAILABLE Main thread -
Open / Fork / Join / Close 158 Copyright ©
2026, Oracle and/or its affiliates StructuredTaskScope 3) scope.join() (this is a blocking call) 1) The subtask state is updated to FAILED Subtask: FAILED Subtask: SUCCESS Subtask: UNAVAILABLE Subtask: UNAVAILABLE Main thread -
Open / Fork / Join / Close 159 Copyright ©
2026, Oracle and/or its affiliates StructuredTaskScope 3) scope.join() (this is a blocking call) 1) The subtask state is updated to FAILED 2) The exception is copied to the subtask 3) onComplete(subtask) is called 4) The VT dies Subtask: FAILED Subtask: SUCCESS Subtask: UNAVAILABLE Subtask: UNAVAILABLE Main thread -
Open / Fork / Join / Close 160 Copyright ©
2026, Oracle and/or its affiliates StructuredTaskScope 3) scope.join() (this is a blocking call) Subtask: FAILED Subtask: SUCCESS Subtask: UNAVAILABLE Subtask: UNAVAILABLE Main thread -
Open / Fork / Join / Close 161 Copyright ©
2026, Oracle and/or its affiliates StructuredTaskScope 3) scope.join() (this is a blocking call) Subtask: FAILED Subtask: SUCCESS Subtask: UNAVAILABLE Subtask: SUCCESS Main thread -
Open / Fork / Join / Close 162 Copyright ©
2026, Oracle and/or its affiliates StructuredTaskScope 3) scope.join() (this is a blocking call) Subtask: FAILED Subtask: SUCCESS Subtask: SUCCESS Subtask: SUCCESS Main thread 1) Calls joiner.result() 2) join() returns the result -
Open / Fork / Join / Close 163 Copyright ©
2026, Oracle and/or its affiliates StructuredTaskScope 4) scope.join() returns the result Subtask: FAILED Subtask: SUCCESS Subtask: SUCCESS Subtask: SUCCESS Main thread -
Open / Fork / Join / Close 164 Copyright ©
2026, Oracle and/or its affiliates 5) Exits the try block, scope.close() is called Main thread -
Cancelling a STS – 1 165 Copyright © 2026, Oracle
and/or its affiliates 2) scope.fork(task) For the next task onFork() returns true StructuredTaskScope Main thread Subtask: UNAVAILABLE Subtask: UNAVAILABLE It cancels the STS -
Cancelling a STS – 2 166 Copyright © 2026, Oracle
and/or its affiliates StructuredTaskScope 3) scope.join() (this is a blocking call) onComplete() returns true Subtask: FAILED Subtask: SUCCESS Subtask: SUCCESS Subtask: UNAVAILABLE Main thread It also cancels the STS -
Cancelling a STS When a STS is cancelled: 1) All
the virtual threads are interrupted 2) onComplete() is not called anymore 3) joiner.result() is called 4) sts.join() returns 167 Copyright © 2026, Oracle and/or its affiliates -
Cancelling a STS Warning: some tasks may complete before their
thread is interrupted So you may have subtasks in SUCCESS or FAILED but onComplete() was not called for them 168 Copyright © 2026, Oracle and/or its affiliates -
Closing a StructuredTaskScope The call to close() waits for all
the threads to be done If some task does not stop when interrupted, then close() does not return At least you have a stack trace that takes you where it happens! 169 Copyright © 2026, Oracle and/or its affiliates -
Dealing with Timeouts 170 Copyright © 2026, Oracle and/or its
affiliates StructuredTaskScope 3) scope.join() (this is a blocking call) Subtask: UNAVAILABLE Subtask: SUCCESS Subtask: UNAVAILABLE Subtask: FAILED Main thread -
Dealing with Timeouts 171 Copyright © 2026, Oracle and/or its
affiliates StructuredTaskScope 3) scope.join() (this is a blocking call) Subtask: UNAVAILABLE Subtask: SUCCESS Subtask: UNAVAILABLE Subtask: FAILED Main thread Time is up! -
Dealing with Timeouts 172 Copyright © 2026, Oracle and/or its
affiliates StructuredTaskScope 3) scope.join() (this is a blocking call) Subtask: UNAVAILABLE Subtask: SUCCESS Subtask: UNAVAILABLE Subtask: FAILED Main thread The default behavior is: 1) The STS is cancelled 3) It interrupts the VTs 4) joiner.onTimeout() is called, throws a TimeoutException 5) You can catch it and analyze the subtasks -
Dealing with Timeouts 173 Copyright © 2026, Oracle and/or its
affiliates StructuredTaskScope 3) scope.join() (this is a blocking call) Subtask: UNAVAILABLE Subtask: SUCCESS Subtask: UNAVAILABLE Subtask: FAILED Main thread You can set a Joiner: 1) The STS is cancelled 2) joiner.onTimeout() is called 3) If you do not to throw the TimeoutException 4) joiner.result() is called 5) scope.join() returns the result -
Dealing with Timeouts 174 Copyright © 2026, Oracle and/or its
affiliates 5) Exits the try block and closes the STS Main thread -
Dealing with Timeouts STS.join() calls Joiner.onTimeout() The default behavior is
that Joiner.onTimeout() throws a TimeoutException So Joiner.result() is not called, and STS.join() throws this TimeoutException You can catch this exception and analyze the results from the subtasks 175 Copyright © 2026, Oracle and/or its affiliates -
Dealing with Timeouts You can implement your own strategy with
a Joiner If your Joiner.onTimeout() method does not throw Then Joiner.result() is called, and you can create a result from the subtasks STS.join() returns the result returned by Joiner.result() 176 Copyright © 2026, Oracle and/or its affiliates -
Role of a Joiner The Joiner defines the following: -
Should the scope be cancelled or not, and when - How to compute a result - What to do with exceptions and timeout It works with: - The result or the exception of each task - A possible timeout 177 Copyright © 2026, Oracle and/or its affiliates -
Available Joiners Cancels the scope if any task fails, join()
throws an exception If all the tasks are successful: result() returns a stream of all subtasks in the order that they were forked 179 Copyright © 2026, Oracle and/or its affiliates Joiner<T, Stream<Subtask<T>> allSuccessfulOrThrow(); -
Available Joiners Throws an exception if all tasks fail Returns
the result of a successful task (no guarantee that it is the first) 180 Copyright © 2026, Oracle and/or its affiliates Joiner<T, T> anySuccessfulOrThrow(); -
Available Joiners The scope is cancelled if the predicate returns
true The predicate is invoked in the onComplete() method returns() returns the stream of all subtasks, in fork order The subtasks can be in the SUCCESS, FAILED, or UNAVAILABLE state 181 Copyright © 2026, Oracle and/or its affiliates Predicate<Subtask<T>> predicate = ...; Joiner<T, Stream<Subtask<T>>> joiner = Joiner.allUntil(predicate); -
Available Joiners return() returns null when all the subtasks complete
successfully join() throws an exception if any of the subtask fails Useful when you submit tasks of different types 182 Copyright © 2026, Oracle and/or its affiliates Joiner<T, Void> awaitAllSuccessfulOrThrow(); -
Available Joiners Returns null Does not throw an exception if
a task fails Useful for subtask that are doing side effects without producing a result 183 Copyright © 2026, Oracle and/or its affiliates Joiner<T, Void> awaitAll(); -
Available Joiners 184 Copyright © 2026, Oracle and/or its affiliates
Wait for all the results Wait on a Predicate Wait for the first result return null return 1 result return all the results awaitSuccessfulOrThrow() awaitAll() anySuccessfulOrThrow() allSuccessfulOrThrow() allUntil(predicate) -
Implementing Your Own Joiner You need to be careful with
race conditions Example: extracting a max 187 Copyright © 2026, Oracle and/or its affiliates class MaxJoiner<T> implements Joiner<T, T> { final Comparator<T> comparator = ...; final List<Subtask<T>> subtasks = new ArrayList<>(); public boolean onFork(Subtask<T> subtask) { this.subtasks.add(subtask); return false; } } -
Implementing Your Own Joiner You need to be careful with
race conditions Example: extracting a max 188 Copyright © 2026, Oracle and/or its affiliates class MaxJoiner<T> implements Joiner<T, T> { final Comparator<T> comparator = ...; final List<Subtask<T>> subtasks = new ArrayList<>(); public T result() { return subtasks.stream() .filter(subtask -> subtask.state() == State.SUCCESS) .map(Subtask::get) .max(comparator).orElseThrow(); } } -
Implementing Your Own Joiner You need to be careful with
race conditions Example: extracting a max 189 Copyright © 2026, Oracle and/or its affiliates class MaxJoiner<T> implements Joiner<T, T> { final Comparator<T> comparator = ...; final List<Subtask<T>> subtasks = new ArrayList<>(); public boolean onComplete(Subtask<T> subtask) { if (subtask.state() == State.SUCCESS) { this.subtasks.add(subtask); } return false; } } -
Implementing Your Own Joiner You need to be careful with
race conditions Example: extracting a max 190 Copyright © 2026, Oracle and/or its affiliates class MaxJoiner<T> implements Joiner<T, T> { final Comparator<T> comparator = ...; final Collection<Subtask<T>> subtasks = new ConcurrentLinkedQueue<>(); public boolean onComplete(Subtask<T> subtask) { if (subtask.state() == State.SUCCESS) { this.subtasks.add(subtask); } return false; } } -
Loom: Where Are We? Virtual Threads Final in 21, improved
in 24 Scoped Values Final in 25 StructuredConcurrency Preview in 25, 26, 27 191 Copyright © 2026, Oracle and/or its affiliates