Java Stable Values 怎么替代双重检查锁
来源:17golang原创
时间:2026-10-05 02:52:07 155浏览 收藏
Java 25 的 Stable Values 可以替代一类典型的双重检查锁:对象需要延迟创建、并发线程只能得到同一个成功初始化的实例,而且初始化完成后不允许再替换。原来由 volatile、synchronized 和两次判空共同保证的语义,可以收敛到 StableValue.supplier(...) 或 StableValue.orElseSet(...)。
但它不是所有锁的通用替代品。StableValue 在 Java 25 中仍是预览 API,值设置后不能重置;需要热更新、主动失效或反复修改的状态仍应使用其他并发工具。
OpenJDK 提案:https://openjdk.org/jeps/502
Java 25 API:https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/lang/StableValue.html
StableValue保存一个最多成功设置一次的值。StableValue.supplier提供线程安全的缓存 Supplier,适合替代静态延迟初始化里的双重检查锁。orElseSet在并发竞争下保证底层 Supplier 成功执行至多一次。- Supplier 抛出异常时不会记录内容,后续调用仍可重试。
为什么双重检查锁容易写成维护负担
假设一个服务启动时不一定用到远程客户端,所以团队把客户端延迟到第一次请求再创建。常见写法是双重检查锁:
public final class ClientRegistry {
private static volatile ApiClient client;
public static ApiClient client() {
ApiClient current = client;
if (current == null) {
synchronized (ClientRegistry.class) {
current = client;
if (current == null) {
// 第二次判空确保只有进入临界区的首个线程创建实例。
current = new ApiClient(loadConfig());
client = current;
}
}
}
// 局部变量避免成功初始化后重复读取 volatile 字段。
return current;
}
private static Config loadConfig() {
// 示例只表达昂贵配置加载,真实项目应处理读取异常。
return new Config("https://service.internal");
}
}
这段代码并非错误,但正确性分散在多个细节里:字段必须是 volatile,锁内必须再次判空,构造完成后才能发布引用,维护者还要理解局部变量为何存在。一次看似无害的重构,例如移除 volatile 或第二次检查,都可能破坏并发语义。
Stable Values 的设计动机,是把“晚一点初始化”与“初始化后像 final 一样稳定”放进标准 API。调用者只表达如何创建值,JDK 负责一次性存储、并发协调与可见性。
从双重检查锁改成 StableValue.supplier
如果调用方只需要一个 Supplier,最直接的改写是 StableValue.supplier:
import java.util.function.Supplier;
public final class ClientRegistry {
private static final Supplier CLIENT =
StableValue.supplier(ClientRegistry::createClient);
public static ApiClient client() {
// 第一次 get 延迟创建,后续 get 返回同一个成功记录的实例。
return CLIENT.get();
}
private static ApiClient createClient() {
// 初始化逻辑只在首次成功计算时执行一次。
return new ApiClient(new Config("https://service.internal"));
}
}
字段仍然声明为 static final,但它保存的是稳定 Supplier,而不是提前创建的客户端。第一次调用 get() 才会运行底层 Supplier。多个线程同时到达时,竞争线程会等待计算完成,然后观察到同一个已记录值,不需要业务代码自己维护 volatile 和同步块。

实例字段可以直接使用 StableValue.orElseSet
若类内部需要观察“尚未设置”和“已经设置”两种状态,可以保留 StableValue holder,并在访问器中调用 orElseSet。官方建议通常把它作为私有字段,不直接暴露给调用方。
public final class DocumentService {
private final Config config;
private final StableValue parser = StableValue.of();
public DocumentService(Config config) {
this.config = config;
}
public Parser parser() {
// 未设置时原子计算并保存;已设置时直接返回已有 Parser。
return parser.orElseSet(() -> new Parser(config));
}
public boolean parserReady() {
// isSet 只查看 holder 状态,不触发初始化。
return parser.isSet();
}
}
StableValue.of() 创建未设置的 holder。orElseSet 成功返回时,内容一定已经设置;成功写入与之后的成功读取之间存在 happens-before 关系,因此读取线程能看到初始化完成的对象状态。
并发语义与异常边界
Stable Values 替代双重检查锁时,最重要的不只是代码变短,而是初始化规则发生了明确约束:
| 场景 | Stable Value 行为 | 迁移时要确认 |
|---|---|---|
| 多个线程同时首次访问 | 最多一个成功的 Supplier 计算被记录,其他线程等待并读取该值 | 初始化允许竞争线程暂时阻塞 |
| Supplier 正常返回 | 值被永久记录,之后不能替换 | 对象生命周期确实是一次性初始化 |
| Supplier 抛出异常 | 异常返回给当前调用者,不记录内容 | 后续调用可能再次执行初始化副作用 |
| 需要清空或重新加载 | 不支持重置已设置内容 | 改用原子引用、缓存或显式生命周期组件 |
“成功至多一次”并不等于“尝试至多一次”。如果初始化先写入外部系统再抛异常,下次调用会重新尝试,副作用可能重复。迁移前应让 Supplier 尽量幂等,或者把不可重复操作放到有事务或去重保证的组件中。

旧代码的影响与采用边界
适合迁移的代码通常同时满足四个条件:对象创建成本较高、确实需要延迟初始化、成功后引用不再变化、所有并发调用者应该共享同一个结果。数据库元数据、解析器、配置派生对象和客户端工厂都可能符合这种模式。
以下情况不建议直接替换:
- 需要刷新:令牌、动态配置、可失效缓存和连接状态需要更新,不能放进只能设置一次的 holder。
- 只要静态单例:初始化按需持有者模式已经简单、成熟且不依赖预览 API,没有必要为了新 API 强行迁移。
- 必须跨旧 JDK 运行:
StableValue从 Java 25 开始提供,旧运行时无法加载相关字节码。 - 公共 API 要长期稳定:预览 API 的签名和形态可能在后续版本变化,最好封装在私有实现层。
Stable Values 还允许 JVM 把已设置的稳定值视作可优化的常量候选,但不应仅凭这个能力承诺业务性能提升。迁移的首要收益是更清晰的并发语义和更少的手写同步代码。
启用预览 API 并做最小验证
Java 25 使用 Stable Values 时,编译和运行都要启用预览特性:
# 使用 JDK 25 编译,并显式启用预览 API。
javac --release 25 --enable-preview StableValueDemo.java
# 运行阶段同样必须启用预览特性。
java --enable-preview StableValueDemo
下面的最小程序让多个虚拟线程同时访问同一个稳定 Supplier,并统计构造次数与对象身份。它用于验证迁移后的关键契约,不代表性能基准。
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Supplier;
import java.util.stream.IntStream;
public class StableValueDemo {
private static final AtomicInteger BUILDS = new AtomicInteger();
private static final Supplier CLIENT = StableValue.supplier(() -> {
// 记录初始化函数被成功执行的次数。
BUILDS.incrementAndGet();
return new Client();
});
public static void main(String[] args) throws Exception {
Set identities = ConcurrentHashMap.newKeySet();
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
var tasks = IntStream.range(0, 100)
.mapToObj(i -> (Runnable) () -> {
// 并发读取稳定 Supplier,并收集对象身份。
identities.add(System.identityHashCode(CLIENT.get()));
})
.toList();
for (Runnable task : tasks) {
executor.submit(task);
}
}
// 预期两个数字都为 1:一个实例身份,一次成功构造。
System.out.println("实例种类数: " + identities.size());
System.out.println("成功构造数: " + BUILDS.get());
}
static final class Client {
// 示例对象不含业务逻辑,只用于观察延迟初始化身份。
}
}
预期输出是“实例种类数: 1”和“成功构造数: 1”。真实项目还应增加一个失败 Supplier 测试,确认异常后是否允许重试,以及重复副作用是否已经得到控制。
迁移建议
- 先确认旧双重检查锁只承担一次性延迟初始化,不承担刷新、销毁或版本切换。
- 静态访问优先改为私有
static final Supplier,实例访问按需使用私有StableValue。 - 把构造逻辑放进可单独测试的 Supplier,并明确失败是否允许重试。
- 在构建、测试、启动脚本里同时加入 Java 25 的
--enable-preview。 - 不要把 StableValue holder 直接暴露为公共 API,为后续预览变更保留替换空间。
如果这些条件都满足,Stable Values 能把双重检查锁从“每个团队都要重新证明正确”的代码模式,变成由平台提供的一次性延迟初始化契约。对尚未准备采用预览 API 的生产项目,保留现有正确实现或使用初始化按需持有者模式仍然是合理选择。
相关问题
StableValue.supplier 和 orElseSet 有什么区别?
supplier 返回普通 Supplier,调用方只需 get();orElseSet 直接操作 holder,适合类内部还要使用 isSet 等状态方法的场景。
Supplier 抛异常后会缓存异常吗?
不会。异常会抛给当前调用者,内容不被记录,之后的调用可以再次尝试计算。
StableValue 可以替代 volatile 吗?
只能替代“一次成功设置后永不变化”这一类用法。持续变化的共享状态仍需要 volatile、原子类、锁或其他同步方案。
Java 25 不启用预览能使用吗?
不能。编译和运行包含 Stable Values 的代码时都必须启用预览特性。
-
文章 · java教程 | 3星期前 | Java · 异常处理 · 资源管理 · java try-with-resources AutoCloseable close suppressed exception501 收藏
-
501 收藏
-
501 收藏
-
501 收藏
-
501 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习