在多线程场景下,延迟加载(Lazy Initialization)一直是检验Java工程师功底的关键点之一。
为了实现单例或按需初始化,最常见的做法就是使用双重检查锁定(Double-Checked Locking, DCL)。
为了防止指令重排,我们不得不引入 volatile 关键字:
// 经典的 DCL 模式,你可能已经写了十年
private volatile Config config;
public Config getConfig() {
Config result = config;
if (result == null) {
synchronized(this) {
result = config;
if (result == null) {
config = result = loadConfig();
}
}
}
return result;
}
这段代码虽然经典,却存在两个明显的痛点:
- 样板代码冗余:逻辑比较绕,稍不留神漏写
volatile 或内层判断,就可能引入难以排查的并发问题。
- 性能天花板:由于字段是
volatile 且非 final,JVM 的 JIT 编译器无法对其进行深度优化(如常量折叠),每次读取都需经过内存屏障。
终结者登场:JEP 526 LazyConstants
在 JDK 26 中,Java 官方提供了一个“标准答案”:LazyConstant<T>。
现在的写法变得非常简洁:
// 代码瞬间清爽,且支持 final 语义
private final LazyConstant<Config> config = LazyConstant.of(() -> loadConfig());
public Config getConfig() {
return config.get(); // 线程安全,官方保证只执行一次
}
为什么 LazyConstant 远强于 volatile?
可能有人会疑惑:这不就是封装了一层吗?自己写个工具类不行吗?还真不一样。
LazyConstant 在 JVM 层面做了三件关键的事:
1. 真正的“常量”性能(@Stable)
LazyConstant 内部使用了 JVM 的私有注解 @Stable。
- volatile:告诉 CPU 每次都要去内存查看,防止值被意外修改。
- LazyConstant:告诉 JIT 编译器,这个值初始化后就不会改变,编译器可以将其内联到调用处。这种“常量折叠”优化在热点代码中能带来显著的性能提升。
2. 容错与重试机制
传统的 DCL 实现中,如果 loadConfig() 抛出异常,处理起来会比较尴尬:是缓存这个异常,还是下次重试?LazyConstant 采用了计算失败不缓存的策略。如果第一次初始化失败,后续线程会再次尝试,直到成功为止。这对于加载数据库连接、网络配置等不稳定资源非常友好。
3. 强制非空(Null-Safety)
LazyConstant 不允许初始化返回 null。这从工程实践层面帮助开发者规避了 NullPointerException 的风险,使代码更加健壮。
进阶:集合的“懒加载”化
除了单个对象,JDK 26 还提供了集合的懒加载支持:
List.ofLazy(() -> loadList())
Map.ofLazy(() -> loadMap())
这意味着你可以定义一个庞大的配置 Map,但在应用启动时实现零开销,只有当你真正调用 get("key") 时,对应的元素才会被计算出来。
// List 示例
class Application {
// 旧方式:
// static final OrderController ORDERS = new OrderController();
// 新方式:
static final List<OrderController> ORDERS
= List.ofLazy(POOL_SIZE, _ -> new OrderController());
public static OrderController orders() {
long index = Thread.currentThread().threadId() % POOL_SIZE;
return ORDERS.get((int)index);
}
}
// Map 示例
class Application {
// 旧方式:
static final Map<String, OrderController> ORDERS
= Map.ofLazy(Set.of("Customers", "Internal", "Testing"),
_ -> new OrderController());
// 新方式:
public static OrderController orders() {
String threadName = Thread.currentThread().getName();
return ORDERS.get(threadName);
}
}
总结
作为开发者,我们可以看出 Java 语言进化的一个清晰脉络:从“手动挡”逐渐转向“自动挡”。
- JDK 8 引入了 Lambda,简化了匿名内部类的使用;
- JDK 21 引入了虚拟线程,减轻了线程池的管理负担;
- JDK 26 的
LazyConstant,则旨在简化并发编程中的状态管理。
以后在面试中被问到如何实现单例模式时,你可以先分析传统 DCL 的不足,再引出 JDK 26 的 LazyConstant 方案。这样的回答既能体现技术深度,也展示了你对语言新特性的关注。
如果你想深入探讨更多 Java 或并发编程的相关话题,欢迎访问云栈社区与其他开发者交流学习。