找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

613

积分

0

好友

79

主题
发表于 昨天 23:03 | 查看: 0| 回复: 0

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);
    }
}

关键点

  1. logger 字段本身是 final 的(指向同一个 LazyConstant 对象);
  2. 首次调用 logger.get() 时才执行lambda表达式,且由JVM保证线程安全;
  3. 初始化后,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 进行实验时,值得注意以下几个技术细节:

  1. 配合 final 使用:为了让JVM进行常量折叠(Constant Folding)优化,持有 LazyConstant 的字段需要声明为 final
  2. 禁止 null:提案设计中明确禁止计算函数返回 null,这与 Optional 或不可变集合的行为保持一致。
  3. 计算函数的行为:虽然JVM保证计算函数最多执行一次(在成功初始化的情况下),但开发者应留意计算函数中的潜在副作用。
  4. 聚合支持:除了单个值,提案还扩展了 ListMap 接口(如 List.ofLazy),用于处理集合元素的延迟初始化。

总结

JEP 526提出的惰性常量试图优化Java延迟初始化的实现方式:

  • 简化代码:通过标准API替代了复杂的双重检查锁定(DCL)或占位类模式。
  • 性能潜力:允许JVM对延迟初始化的值进行常量折叠优化,这是普通 volatile 字段无法做到的。
  • 安全性:将线程同步的复杂性下沉到JDK内部实现。

对于追求不可变性和极致性能的开发者来说,这无疑是一个值得期待的特性。如果你想深入了解更多关于Java或系统设计的最佳实践,欢迎在云栈社区交流讨论。




上一篇:Siri 接入 Google Gemini 解析:苹果的隐私承诺与技术妥协
下一篇:Java操作数据库必备:JDBC核心组件与MySQL 8.0完整指南
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-1-25 18:13 , Processed in 0.311819 second(s), 42 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表