Java 26:什么是 Lazy Constants,为什么它们让 double-checked locking 失效(JEP 526)
Source: Dev.to
请提供您希望翻译的具体文本内容,我将为您翻译成简体中文并保留原有的格式、Markdown 语法以及技术术语。谢谢!
什么是 Java 26 的 JEP 526?
JEP 526 引入了 java.lang.LazyConstant 类:一个保存 不可变 值的容器,最多只会初始化一次。
| 版本 | 特性名称 | 备注 |
|---|---|---|
| Java 25 | Stable Values (JEP 502) | API 更冗长,仍在预览中 |
| Java 26 | Lazy Constants (JEP 526) | API 简化,第二次预览 |
final保证不可变,但要求立即初始化。- 非
final字段允许延迟初始化,但失去并发安全保证。
LazyConstant 结合了两者的优点:你可以在需要时进行初始化,但 仅一次,并拥有与 final 相同的安全性。
为什么存在双重检查锁定以及它的问题?
所有 Java 开发者都写过类似的代码:
public class MetricRegistry {
private static volatile MetricRegistry instance;
public static MetricRegistry getInstance() {
if (instance == null) {
synchronized (MetricRegistry.class) {
if (instance == null) {
instance = new MetricRegistry();
}
}
}
return instance;
}
}它可以工作,但需要:
volatile- 两次
null检查 - 一个
synchronized块
第一次阅读这段代码的人通常会感到困惑。使用静态内部类的替代方案更安全,但仍然是一种 变通办法。这些选项都不是对真实问题的“一等抽象”:“在需要时安全地初始化一次”。
LazyConstant 实际是如何工作的?
想象一个支付服务,它使用一个创建成本很高的 HTTP 客户端,只有在路由被调用时才有意义存在:
public class PaymentService {
private final LazyConstant httpClient =
LazyConstant.of(() -> HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(5))
.build());
public String charge(String orderId, BigDecimal amount) {
HttpClient client = httpClient.get(); // 第一次调用会创建客户端
// 使用 client 调用网关
…
}
}HttpClient在第一次 调用httpClient.get()时创建。- 随后对
get()的调用,JVM 会返回同一个值(甚至可能进行 constant‑folding)。 - 线程安全,无需
synchronized或volatile。
Java 25 到 Java 26 在 JEP 526 中有什么变化?
- Java 25 – 低层 API(
setOrThrow、trySet、orElseSet),将资源转化为同步原语。 - Java 26 – 仅保留
LazyConstant.of(supplier)。你提供初始化函数,JVM 负责其余工作。
“惰性”工厂用于集合
惰性列表和映射的工厂方法位置已更改,从 StableValue 移到直接在 List 和 Map 上:
// 具有 10 个槽位的惰性列表,每个槽位独立初始化
List processors = List.ofLazy(10, _ -> new OrderProcessor());
// 具有预定义键的惰性映射,值按需初始化
Map limiters = Map.ofLazy(
Set.of("payments", "refunds", "reports"),
key -> RateLimiter.create(requestsPerSecond(key))
);列表的每个槽位和映射的每个条目都有其 独立的初始化周期。payments 槽可以存在,而 refunds 尚未创建。
LazyConstant 与普通 final 字段有什么区别?
| 特性 | final | LazyConstant |
|---|---|---|
| 初始化时机 | 在构造函数中(立即) | 在第一次调用 .get() 时 |
| 同步需求 | 不需要(已经是线程安全的) | 内部实现线程安全 |
| 创建成本 | 总是支付,即使从未使用 | 仅在使用时 支付 |
| 灵活性 | 值必须在构造时准备好 | 可以依赖尚未存在的资源 |
// final 强制立即初始化
private final ExpensiveClient client = new ExpensiveClient(); // 在构造函数中执行
// LazyConstant 在第一次调用 .get() 时初始化
private final LazyConstant<ExpensiveClient> client =
LazyConstant.of(ExpensiveClient::new);初始化后,JVM 可以把 LazyConstant 当作常量处理,并应用与 static final 相同的优化。
在 Java 26 中使用 Lazy Constants 前需要了解什么?
- 仍然是预览版 – 要编译和运行,请启用预览:
javac --enable-preview --release 26 PaymentService.java
java --enable-preview PaymentServiceLazyConstant不是Serializable。如果需要序列化该对象,它将无法工作。- 存储的值是 不可变 的,但内部对象可能是可变的。
LazyConstant确保 引用 只被赋值一次;之后对对象的操作由你自行负责。
何时使用(以及何时不使用)LazyConstant?
使用 的情形:
- 对象创建成本高且并非总是必需。
- 需要线程安全且不想手动管理同步。
- 示例:数据库连接、HTTP 客户端、配置缓存、度量注册器。
不使用 的情形:
- 一个普通的
final就能解决问题。 - 需要序列化。
- 想要精确控制初始化时机(例如,在应用启动的某个特定点之前)。
LazyConstant 确保初始化在第一次调用 .get() 之前完成,但不会在你自行选择的任意时刻进行初始化。
为什么这在现在很重要,尽管仍在预览阶段?
double‑checked locking 正在成千上万的 Java 系统中投入生产。并不是因为有人喜欢这种复杂性,而是因为 没有更好的替代方案。JEP 526 是 OpenJDK 对此问题的回应。
该特性在最终确定之前仍可能会改变,第二次预览正是为了收集反馈。然而,方向已经明确:平台希望将“惰性”初始化视为 一等原语,从而消除需要易出错的样板代码模式。
LazyConstant – 一等抽象
使用该特性作为一等抽象,得到编译器和 JVM 的支持,而不是每个开发者自行实现的“代码配方”。现在使用 --enable-preview 进行实验,是在该特性正式稳定之前直接了解其对代码影响的最佳方式。
关于 LazyConstant 和 JEP 526 在 Java 26 中的常见问题
LazyConstant 是线程安全的吗?
是的。唯一的初始化在多线程环境下有效,无需 synchronized 或 volatile。
我可以将 LazyConstant 与 Serializable 一起使用吗?
不能。如果对象需要序列化,请使用 final 或其他机制。
LazyConstant 能取代使用静态内部类的 Singleton 模式吗?
在大多数情况下可以。结果相同,但代码更简洁,意图在类型上更明确。
使用此特性需要 Java 26 吗?
需要,且必须使用 --enable-preview。JEP 526 仍处于 second preview 阶段,尚未稳定。
如果初始化函数抛出异常会怎样?
异常会传播给调用 .get() 的代码,且 LazyConstant 不会被标记为已初始化。下次调用会再次尝试。
关于作者
我是 Luis De Llamas,act digital 的 Developer Advocate,Oracle ACE 与 IBM Champion。
如果想了解更多关于 Quarkus、Java 以及生态系统的内容,请在这里找到我: