Scoped values go final

Java 25 Course · lesson 3 of 16 · 4 min read

The supported replacement for ThreadLocal in a world of millions of threads.

Open this lesson in the learning hub

Key points

  • JEP 506 finalises scoped values after four previews - no flag needed on 25.
  • A scoped value is immutable and bound only for the duration of one call.
  • That removes the two ThreadLocal failure modes: forgetting remove(), and per-thread copies.
  • Child threads inside the scope share the binding by reference rather than copying it.
  • Rebinding is nested and automatic - the outer value returns when the inner block ends.

Example

// Java 25 (JEP 506) - final, no --enable-preview needed:
//
//   static final ScopedValue<String> USER = ScopedValue.newInstance();
//
//   ScopedValue.where(USER, "ada").run(() -> handle());   // USER.get() == "ada"
//   // outside the block USER is unbound - get() throws
//
// The Java 21 equivalent, with the cleanup you must never forget:
public class Main {
    static final ThreadLocal<String> USER = new ThreadLocal<>();

    static void handle() {
        System.out.println("serving " + USER.get());
    }

    public static void main(String[] args) {
        USER.set("ada");
        try {
            handle();
        } finally {
            USER.remove();      // forget this on a pooled thread and it leaks to the next task
        }
        System.out.println("after remove(): " + USER.get());
        System.out.println("a scoped value unbinds itself - there is no remove() to forget");
    }
}

Scoped values make the cleanup automatic, which is the bug ThreadLocal could never design away.

This is a reading copy. The full lesson — with the visual explainer, the interactive lab and a Run button for the code — lives in the Java 25 Course course, and every lesson in it is listed on the Java 25 Course contents page.