1. 背景
开发者常面临一个两难选择:是追求不可变性(Immutability),还是追求灵活的延迟初始化(Lazy Initialization)?
现阶段,通常使用 static final 字段来修饰单例或常量(参考文章:熟悉的味道——从Java单例写到C++单例)。这虽然保证了不可变性和线程安全,但也意味着必须在类加载或对象构造时立即初始化,无法按需加载。 为解决此问题,JEP 526在Java 26中作为第二次预览特性引入了惰性常量(Lazy Constants)。
提案目标:提供一种机制,既能像 final 字段一样享受JVM的常量折叠优化,又能由JVM保证线程安全的按需初始化。
2. 为什么需要惰性常量?
2.1. 传统方式的问题
2.1.1. 方式 1:使用 final 字段(立即初始化)
public class OrderController {
// 必须在构造时初始化
private final Logger logger = Logger.create(OrderController.class);
public void submitOrder(User user, List<Product> products) {
logger.info("订单开始");
// ...
}
}
问题:
- ❌ 每次创建
OrderController 均需要立即初始化logger字段;
- ❌ 启动慢:实际可能包含规则引擎等引用,均需要初始化对应资源。
2.1.2. 方式 2:使用可变字段(手动延迟初始化)
public class OrderController {
private Logger logger = null;
private Logger getLogger() {
if (logger == null) {
logger = Logger.create(OrderController.class);
}
return logger;
}
public void submitOrder(User user, List<Product> products) {
getLogger().info("订单开始");
// ...
}
}
问题:
- ❌ 线程不安全:并发调用可能创建多个logger;
- ❌ 性能损失:JVM无法对非final字段做常量折叠优化;
- ❌ 维护困难:必须记得用
getLogger() 而非直接访问字段。
2.2. 三种方式对比
| 特性 |
final 字段 |
LazyConstant |
普通可变字段 |
| 更新次数 |
恰好 1 次 |
最多 1 次 |
无限制 |
| 初始化时机 |
构造时 |
首次访问时 |
任意时刻 |
| 线程安全 |
✅ |
✅ |
❌ |
| 常量折叠优化 |
✅ |
✅ |
❌ |
| 灵活性 |
❌ |
✅ |
✅ |
3. JEP 526 引入的解决方案
3.1. LazyConstant 的使用
注:为了演示方便,以下案例不展示具体的运行输出(当前示例可以在IDEA中轻松复现)。
JEP 526引入了 java.lang.LazyConstant 类。以下是使用示例:
public class OrderController {
// 使用 LazyConstant 延迟初始化 logger
private final LazyConstant<Logger> logger
= LazyConstant.of(() -> Logger.create(OrderController.class));
public void submitOrder(User user, List<Product> products) {
logger.get().info("订单开始 - 用户: " + user.name());
double total = products.stream()
.mapToDouble(Product::price)
.sum();
logger.get().info("订单提交 - 总金额: ¥" + total);
}
}
关键点:
logger 字段本身是 final 的(指向同一个 LazyConstant 对象);
- 首次调用
logger.get() 时才执行lambda表达式,且由JVM保证线程安全;
- 初始化后,JVM可以对logger字段应用常量折叠优化。
3.2. 生产场景挑战:Spring Boot 应用的启动
在实际生产环境中,这也是Spring Boot等框架面临的典型问题。当一个服务依赖大量其他组件时,默认的立即初始化会导致应用启动缓慢。
@Service
public class DashboardService {
// 典型的 Spring 组件,依赖众多的下游服务
// 默认情况下,Spring容器启动时会初始化所有这些Bean
private final UserService userService;
private final OrderService orderService;
private final ProductRepository productRepo;
private final AnalyticsService analyticsService;
private final NotificationService notificationService;
// ... 可能还有更多依赖
public DashboardService(UserService userService, OrderService orderService,
ProductRepository productRepo, AnalyticsService analyticsService,
NotificationService notificationService) {
this.userService = userService;
this.orderService = orderService;
this.productRepo = productRepo;
this.analyticsService = analyticsService;
this.notificationService = notificationService;
}
}
在目前的Spring框架中,我们通常使用 @Lazy 注解来缓解这个问题,但这依赖于动态代理,并且增加了运行时的复杂性。 JEP 526 提供的 LazyConstant 为这种模式提供了一种更底层、更高效的原生支持。未来,依赖注入框架可能会利用这一特性,将字段类型的 T 替换为 LazyConstant<T>,从而在不牺牲 final 语义和性能的前提下极大优化启动速度。
4. 聚合惰性常量:处理集合场景
4.1. 惰性列表(Lazy List)
当需要一个对象池时,惰性列表非常有用:
public class Application {
private static final int POOL_SIZE = 10;
// 创建惰性列表,每个元素按需初始化
static final List<OrderController> ORDERS =
List.ofLazy(POOL_SIZE, index -> {
System.out.println("初始化 OrderController[" + index + "]");
return new OrderController();
});
public static OrderController orders() {
long index = Thread.currentThread().threadId() % POOL_SIZE;
return ORDERS.get((int) index);
}
}
4.2. 惰性映射(Lazy Map)
对于更复杂的键值对场景:
public class Application {
static final Map<String, OrderController> ORDERS =
Map.ofLazy(
Set.of("Customers", "Internal", "Testing"),
threadType -> {
System.out.println("为 " + threadType + " 创建控制器");
return new OrderController();
}
);
public static OrderController orders() {
String threadName = Thread.currentThread().getName();
return ORDERS.get(threadName);
}
}
5. 传统延迟初始化方式的陷阱
5.1. 类持有者惯用法(Holder Class Idiom)
public class OrderController {
public static Logger getLogger() {
class Holder {
private static final Logger LOGGER = Logger.create(OrderController.class);
}
return Holder.LOGGER;
}
}
缺点:
- ❌ 仅适用于静态字段;
- 🗂️ 多个字段需要多个持有者类,增加了内存开销;
- 📈 启动时间随持有者类数量增加。
5.2. 通过ConcurrentHashMap保证初始化
public class OrderController {
private final Map<Class<?>, Logger> loggers = new ConcurrentHashMap<>();
public Logger getLogger() {
return loggers.computeIfAbsent(
OrderController.class,
Logger::create
);
}
}
缺点:
- 📉 Map操作开销大;
- ❌ 无法做常量折叠优化;
- 🔧 需要额外的数据结构。
5.3. 双重检查
class OrderController {
private volatile Logger logger;
public Logger getLogger() {
Logger v = logger;
if (v == null) {
synchronized (this) {
v = logger;
if (v == null) {
logger = Logger.create(...);
}
}
}
return v;
}
}
缺点:
- 由于
logger 是一个可变字段,此处无法应用常量折叠优化。
- 为了使双重检查惯用法工作,
logger 字段必须声明为 volatile(以便保证跨多个线程一致地读取和更新字段的值)。
5.4. 不可变数组的双重检查
class OrderController {
private static final VarHandle LOGGERS_HANDLE
= MethodHandles.arrayElementVarHandle(Logger[].class);
private final Object[] mutexes;
private final Logger[] loggers;
public OrderController(int size) {
this.mutexes = Stream.generate(Object::new).limit(size).toArray();
this.loggers = new Logger[size];
}
public Logger getLogger(int index) {
// 这里需要 volatile 以保证我们只看到完全初始化的元素对象
Logger v = (Logger)LOGGERS_HANDLE.getVolatile(loggers, index);
if (v == null) {
// 对每个索引使用不同的互斥对象
synchronized (mutexes[index]) {
// 普通读取在此处足够,因为对元素的更新总是在与此读取相同的互斥下进行
v = loggers[index];
if (v == null) {
// 这里需要 volatile 以建立与未来 volatile 读取的 happens-before 关系
LOGGERS_HANDLE.setVolatile(loggers, index,
v = Logger.create(... index ...));
}
}
}
return v;
}
}
缺点:
- 😱 代码极其复杂,容易实现错误;
- 🔒 需要为每个元素单独同步,性能较差。
6. 技术细节与约束
在使用 JEP 526 进行实验时,值得注意以下几个技术细节:
- 配合
final 使用:为了让JVM进行常量折叠(Constant Folding)优化,持有 LazyConstant 的字段需要声明为 final。
- 禁止
null 值:提案设计中明确禁止计算函数返回 null,这与 Optional 或不可变集合的行为保持一致。
- 计算函数的行为:虽然JVM保证计算函数最多执行一次(在成功初始化的情况下),但开发者应留意计算函数中的潜在副作用。
- 聚合支持:除了单个值,提案还扩展了
List 和 Map 接口(如 List.ofLazy),用于处理集合元素的延迟初始化。
总结
JEP 526提出的惰性常量试图优化Java延迟初始化的实现方式:
- 简化代码:通过标准API替代了复杂的双重检查锁定(DCL)或占位类模式。
- 性能潜力:允许JVM对延迟初始化的值进行常量折叠优化,这是普通
volatile 字段无法做到的。
- 安全性:将线程同步的复杂性下沉到JDK内部实现。
对于追求不可变性和极致性能的开发者来说,这无疑是一个值得期待的特性。如果你想深入了解更多关于Java或系统设计的最佳实践,欢迎在云栈社区交流讨论。