跳转至

Scoped Values(JEP 506)#

版本信息

为什么需要#

JDK 8 里"线程局部数据"用 ThreadLocal:可变、生命周期无界、容易内存泄漏,且与虚拟线程不太合拍(虚拟线程海量,ThreadLocal 的可变共享会破坏它们的不可变性假设)。ScopedValueJEP 506,转正)提供不可变、有界的作用域局部值——值只在一段代码作用域内有效,结束自动消失,天然适合虚拟线程与结构化并发。

语法#

examples/scoped-values/ 真实源码(snippets 嵌入零漂移):

package com.javamodern.scoped;

import java.util.function.Supplier;

/**
 * 演示 JDK 25 Scoped Values(JEP 506,转正)。
 *
 * <p>{@code ScopedValue}(在 {@code java.lang},无需 import)是不可变、有界的"作用域局部"值,
 * 适合与虚拟线程配合,替代 {@code ThreadLocal} 的可变、无界生命周期带来的内存泄漏与线程安全风险。
 */
public class ScopedValueDemo {

    /** 当前用户的作用域值。 */
    public static final ScopedValue<String> USER = ScopedValue.newInstance();

    /** 在以 {@code user} 绑定的作用域内执行 {@code body},返回其结果。 */
    public static <T> T runAs(String user, Supplier<T> body) {
        return ScopedValue.where(USER, user).call(body::get); // (1)
    }

    /** 当前是否处于 USER 绑定的作用域内(作用域外为 false)。 */
    public static boolean isUserBound() {
        return USER.isBound();
    }

    public static void main(String[] args) {
        String who = runAs("alice", () -> "running as " + USER.get());
        System.out.println(who);
        System.out.println("bound outside = " + isUserBound());
    }
}
  1. :material-lightbulb: ScopedValue.where(USER, user).call(body)USER 绑定为 user、在 body 作用域内可见,call 返回后绑定自动解除。ScopedValuejava.lang,无需 import。

运行输出(JDK 25 实测):

running as alice
bound outside = false

与 JDK 8 对比#

static final ThreadLocal<String> USER = new ThreadLocal<>();
USER.set("alice");        // 可变:随时改
try { run(); }
finally { USER.remove(); } // 必须手动 remove,否则泄漏
String u = USER.get();
static final ScopedValue<String> USER = ScopedValue.newInstance();
ScopedValue.where(USER, "alice").run(() -> {  // 不可变,作用域结束自动消失
    String u = USER.get();
});
// 离开作用域:USER.isBound() == false,无需 remove

底层原理#

ScopedValue 的绑定是词法作用域、不可变的:where(V, x).run/call(body) 在进入 body 时建立绑定、退出时(无论正常返回还是抛异常)撤销。作用域内 V.get() 返回绑定值;作用域外 V.isBound()falseV.get()NoSuchElementException。因为没有可变状态,多个虚拟线程可安全共享同一个 ScopedValue,且绑定随调用栈自动清理,不会泄漏。

flowchart TD
    A["ScopedValue.where(USER, 'alice')"] --> B["进入作用域:USER 绑定"]
    B --> C["body 内 USER.get() = 'alice'"]
    C --> D{"作用域结束"}
    D --> E["自动解绑:USER.isBound() = false"]
    D -.body 抛异常.- E
    style A fill:#bbdefb
    style E fill:#ffe0b2

常见坑 / 最佳实践#

  • 不可变ScopedValue 一旦在作用域内绑定就不能改;要"覆盖"用嵌套的 where 再绑一层。
  • 取未绑定值会抛:作用域外 get()NoSuchElementException;不确定时先用 isBound()
  • 优先替代 ThreadLocal:新代码、尤其配合虚拟线程,优先 ScopedValue;遗留库的 ThreadLocal 仍可用,但别在新设计中扩散。
  • run vs callrun(Runnable) 无返回值、call(Callable) 有返回值(可抛受检异常)。

小结#

ScopedValue 用"不可变 + 词法作用域"替代 ThreadLocal 的"可变 + 无界",与虚拟线程、结构化并发(JEP 505)形成配套,是 JDK 25 并发工具箱的关键一环。

参考#