1. 背景
开发者常面临一个两难选择:是追求不可变性(Immutability),还是追求灵活的延迟初始化(Lazy Initialization)?现阶段,通常使用static final字段来修饰单例或常量。这虽然保证了不可变性和线程安全,但也意味着必须在类加载或对象构造时立即初始化,无法按需加载。
为解决此问题,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. 三种方式对比
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 = v = 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内部实现。