skip to content

loom: what's left

structured concurrency going final, and beyond.

chapter
chapter 18.04
reading time
7 min read
preview

Project Loom's big pieces have landed: virtual threads (final in 21), monitors that don't pin (24), and scoped values (25). the last one is structured concurrency: seven previews from 21 to 27, an API that changed shape twice, and JEP 543 proposes making the jdk 27 version final in jdk 28 (as of october 10, 2026, it's in review).

what's left after that is smaller and slower: the remaining pinning cases, and whether you'll ever get to plug in your own scheduler.

the picture

you know how virtual threads made "one thread per request" cheap again, and then you need to do three things concurrently inside that request? that's what's been in preview the longest. cheap threads were the engine; structured concurrency is the steering: a way to start several subtasks and be sure they all end (succeed, fail or get cancelled) before your method returns.

think of building a house. virtual threads hired as many workers as you want for almost nothing. structured concurrency is the site manager who makes sure nobody's still pouring concrete for a house the client already cancelled. the structured concurrency chapter covers how to use it; this one covers how it got here and where Loom goes next.

seven previews, two redesigns

structured concurrency has been in preview longer than almost anything in java's history: incubating in 19 and 20, then a preview in every release from 21 to 27. that's not indecision, it's an API getting real use and being changed because of it. here's the same method in each era:

Handler.java · JEPs 505, 525 · preview
  1. 1Response handle() throws InterruptedException {
  2. 2 try (var scope = StructuredTaskScope.open()) {
  3. 3 Subtask<String> user = scope.fork(() -> findUser());
  4. 4 Subtask<Integer> order = scope.fork(() -> fetchOrder());
  5. 5 scope.join(); // a failure throws FailedException (unchecked)
  6. 6 return new Response(user.get(), order.get());
  7. 7 }
  8. 8}

no more subclasses: open() makes a scope, and the policy is a Joiner you can pass to open(joiner). with the default policy, join() itself throws on failure, as an unchecked StructuredTaskScope.FailedException. code from 24 doesn't compile here. 26 made small changes: one joiner renamed, joiners that handle timeouts, allSuccessfulOrThrow() returning a list.

switch eras. the method does the same thing every time: fork two subtasks, wait for both, fail if either fails. what changed is how you pick the policy (highlighted) and how a failure reaches you.

two redesigns, then: jdk 25 replaced the policy subclasses with open() and joiners, and jdk 27 made failures arrive as a checked ExecutionException, the way Future.get() always did. both times, code written for the previous preview stopped compiling, which is the deal you take with previews (the full rules are in preview features). JEP 543 proposes making the jdk 27 shape final, unchanged. the jdk 25 version above compiles and runs on jdk 25 with --enable-preview; the full working example is in structured concurrency.

quick check
your jdk 25 code wraps scope.join() in a try that catches StructuredTaskScope.FailedException. what's different if JEP 543 ships in jdk 28 as proposed?

why it took this long

structured concurrency touches things that are expensive to get wrong forever: what happens to a subtask when its parent is interrupted, how a failure is reported, how a policy like "first success wins" plugs in, how a thread dump shows the tree of tasks. each preview changed one of those. scoped values went through the same thing (an incubator in 20, four previews, final in 25), and they were designed together: a scope's subtasks see the parent's scoped value bindings, which is what makes the pair a replacement for ThreadLocal-based context passing.

hover a stage.

Loom's arc. the dates are general availability of each release; the last stop is a proposal, not a promise.

what's left

after structured concurrency, Loom's remaining work is quieter: rough edges rather than features.

jdk 28

proposed to finalize the jdk 27 API without further change. as of october 10 it's in review, which ends october 13.

Loom's pieces and where each one stands, checked against the JEP pages, the jdk 28 project page and the loom-dev mailing list (october 2026). click a row.
  • the last pinning cases. JEP 491 fixed synchronized, but a virtual thread that blocks inside a class initializer, or waits for one, still pins its carrier (you can watch it happen in virtual threads, inside). JEP 491 lists these as future work.
  • custom schedulers. the scheduler is a fixed ForkJoinPool today. an experimental pluggable-scheduler API lives in a branch of the Loom repository, and the team's position on loom-dev in 2025 was that it's a research topic, with no plan to expose it yet: a scheduler runs arbitrary code at very sensitive moments.
  • things nobody has proposed. time-slicing (preempting a virtual thread that's hogging its carrier): JEP 444 noted the scheduler doesn't do it, and no JEP since has proposed it. virtual threads remain cooperative: they give up their carrier when they block.
keylines
  • Loom shipped: virtual threads (21), monitors that don't pin (24), scoped values (25).
  • structured concurrency: previews from 21 to 27, JEP 543 proposes it final in jdk 28 (as of october 2026).
  • the API changed twice: open() and joiners in 25, a checked ExecutionException from join() in 27.
  • still open: pinning in class initializers, and custom schedulers (research, not planned).
  • virtual threads stay cooperative. no JEP proposes time slicing.