Java基础、中级、高级、架构面试资料

Java 单例模式终于有救!JDK 26 LazyConstants 终结手写线程安全时代

JAVA herman 12浏览
公告:“业余草”微信公众号提供免费CSDN下载服务(只下Java资源),关注业余草微信公众号,添加作者微信:xttblog2,发送下载链接帮助你免费下载!
本博客日IP超过2000,PV 3000 左右,急需赞助商。
极客时间所有课程通过我的二维码购买后返现24元微信红包,请加博主新的微信号:xttblog2,之前的微信号好友位已满,备注:返现
受密码保护的文章请关注“业余草”公众号,回复关键字“0”获得密码
所有面试题(java、前端、数据库、springboot等)一网打尽,请关注文末小程序
视频教程免费领
腾讯云】1核2G5M轻量应用服务器50元首年,高性价比,助您轻松上云

Java 又改了,这次是 JEP 502 稳定值 Stable Values 改为 JEP 526 惰性常量 Lazy Constants 了。

这意味着什么呢?意味着我半年前写的一篇文章《https://mp.weixin.qq.com/s/Y6P08Yzer06Wehj7dc9qcw》,部分内容失效了,其中的类名等做了一些变更。

我看了一下这个https://openjdk.org/jeps/526内容,这些改动被 JDK 社区说的冠冕堂皇的,总之就是言之有理。所以,接下来我们就一起来一次 Java 26 新特性深度解析,从 Stable Values 到 Lazy Constants,单例模式迎来的革新!

延迟初始化的困境

众所周知,在 Java 开发中,我们经常会遇到一个经典矛盾,启动性能 vs 运行时性能

比如,一个大型企业级应用,启动时需要初始化数十个服务组件。

class Application {
    static final OrderController ORDERS = new OrderController();
    static final ProductRepository PRODUCTS = new ProductRepository();
    static final UserService USERS = new UserService();
    // ... 还有几十个
}

每个组件的构造可能涉及数据库连接、配置加载、资源分配等耗时操作,导致应用启动缓慢。但很多时候,某些组件可能在整个应用生命周期中都不会被使用,我们却已经为它们付出了启动时间成本。

此时,推荐的做法或传统的解决方案是使用延迟初始化。

class OrderController {
    private Logger logger = null;

    Logger getLogger() {
        if (logger == null) {
            logger = Logger.create(OrderController.class);
        }
        return logger;
    }
}

但这也带来了新问题。

  1. 线程安全问题:多线程环境下可能创建多个实例
  2. 性能损失:失去final字段的常量折叠优化
  3. 代码丑陋:需要显式判空,增加维护成本

为此,我们可能会使用单例模式,但是要知道单例模式的写法高达 7 种,面试时常被吊打。单例虽然能解决线程安全问题,但是每次可能都有 if 判断,还有无法利用 final 特性,引入代码复杂性等。

于是,JDK 25 引入了Stable Values和 JDK 26 进化的Lazy Constants,就是为了解决这些困境而生。

JEP 502 vs JEP 526

从 JEP 502 Stable Values 到 JEP 526 Lazy Constants,本质是一次重要的 API 重塑。

JDK 25 的 Stable Values

Stable Values 是 JDK 25 的一次尝试,是一个预览特性。

JEP 502 在 JDK 25 中首次引入StableValue API。

class OrderController {
    private final StableValue<Logger> logger = StableValue.of();

    Logger getLogger() {
        return logger.orElseSet(() -> 
            Logger.create(OrderController.class));
    }
}

这个设计解决了上面提到的一些核心问题,但 Java 社区反馈暴露了一些不足。

  • API 层级偏低:orElseSet()等操作过于贴近底层实现
  • 命名不够直观:“StableValue”没有清晰传达“延迟初始化常量”的意图
  • 使用方式繁琐:需要每次都调用orElseSet()方法

所以,JDK 26 进行了改版。

JDK 26 的 Lazy Constants

JDK 26 听取了建议,对这个特性进行了革新,也就是我们现在看到的 Lazy Constants,相当于是第二次预览。

这次预览是基于开发者的反馈,JEP 526 进行了重大调整,可以概括为做减法

首先是,API 核心重命名

// JDK 25
StableValue<Logger> logger = StableValue.of();

// JDK 26
LazyConstant<Logger> logger = LazyConstant.of(() -> 
    Logger.create(OrderController.class));

着背后的考量是,LazyConstant 延迟常量比 StableValue 稳定值更准确地描述了特性本质,这是一个会被延迟初始化但之后不可变的常量

其次是移除底层操作,聚焦高层用例。

JDK 26 移除了以下容易误用的方法。

  • orElseSet(Supplier),需要手动调用初始化
  • setOrThrow(T),底层设置操作
  • trySet(T),尝试设置操作

为什么要这样做呢?因为社区认为这些操作要求开发者理解底层同步机制,容易引发竞态条件。所以,JDK 26 改为强制在构造时提供计算函数,将线程安全完全封装在 API 内部。

接下来是集合 API 的深度集成。将集合相关工厂方法迁移到标准接口。

// JDK 25
StableValue.list(size, creator)

// JDK 26
List.ofLazy(size, creator)  // 更自然的 API
Map.ofLazy(keySet, creator)  // 新增对 Map 的支持

这样做的好处是,提升 API 可发现性,符合 Java 开发者使用习惯。

最后是禁止 null 值。JDK 26 明确禁止计算函数返回 null,这与ScopedValue等现代 API 保持一致,简化实现并提升性能。

Lazy Constants 的核心优势

首先是线程安全的单例初始化。

class Application {
    static final LazyConstant<OrderController> ORDERS = 
        LazyConstant.of(OrderController::new);

    public static OrderController orders() {
        return ORDERS.get(); // 线程安全,仅初始化一次
    }
}

以后的单例,与 synchronized、volatile 等可能就无关了。要知道,这其中比较关键的是:

  • get()方法保证恰好一次初始化
  • 无需synchronized,也无需volatile
  • 即使在高并发场景下也安全

其次是,JVM 常量折叠优化。

LazyConstant存储在final字段中且被初始化后,JIT 可以将其内容视为真正的常量,进行以下优化。

  • 内联展开:直接嵌入常量值
  • 死码消除:基于常量值删除不可达代码
  • 简化计算:编译时完成常量表达式求值

这与 JDK 内部使用的@Stable注解效果相同,但现在是安全、公开的 API

接着是延迟初始化链。

class OrderController {
    // Logger在首次使用时才初始化
    private final LazyConstant<Logger> logger = 
        LazyConstant.of(() -> Logger.create(OrderController.class));

    void submitOrder(User user, List<Product> products) {
        logger.get().info("Order submitted");
    }
}

class Application {
    // OrderController在首次使用时才初始化
    static final LazyConstant<OrderController> ORDERS = 
        LazyConstant.of(OrderController::new);
}

整个依赖链都是按需初始化,极大改善启动性能。但是,启动速度加快了之后,首次访问就会相对的更慢一些。

五种单例模式的新实现

有了 JDK 26,以前的 7 种单例实现,可以改为现在的 5 种单例实现了。当然,以前的 7 中单例实现,也不是不能用。合起来就是 12 种实现了。

接下来,我们就一起看看新的五种单例实现。

静态字段

社区是非常推荐这种静态字段的单例模式的。

public class DatabaseService {
    private static final LazyConstant<DatabaseService> INSTANCE = 
        LazyConstant.of(DatabaseService::new);

    public static DatabaseService getInstance() {
        return INSTANCE.get();
    }

    private DatabaseService() {
        // 昂贵的初始化操作
        connectToDatabase();
    }
}

这种写法的优点是,简洁、线程安全、性能最优,且适合绝大多数单例场景。

枚举单例的增强版

第二种方式,还是使用枚举,同时结合LazyConstant

public enum ConfigManager {
    INSTANCE;

    private final LazyConstant<Config> config = 
        LazyConstant.of(() -> loadConfigFromFile());

    public Config getConfig() {
        return config.get();
    }
}

这种写法的优点是,天然防止反射攻击,配置延迟加载。

带参数的单例池

一次实现或批量实现单例池,用法如下。

public class CacheService {
    private final String cacheName;

    private static final Map<String, LazyConstant<CacheService>> INSTANCES =
        Map.ofLazy(Set.of("user", "product", "order"),
                   name -> new CacheService(name));

    public static CacheService getInstance(String name) {
        return INSTANCES.get(name);
    }

    private CacheService(String cacheName) {
        this.cacheName = cacheName;
        initializeCache();
    }
}

这是 JDK 26 新增的写法,通过Map.ofLazy()实现带参数的单例池。

资源池化

接下来,针对各种连接池,可以进行资源池化了。

class ConnectionPool {
    static final List<LazyConstant<Connection>> POOL = 
        List.ofLazy(10, _ -> createConnection());

    public static Connection getConnection() {
        long index = Thread.currentThread().threadId() % 10;
        return POOL.get((int)index).get();
    }
}

这个适用场景就比较多了,数据库连接池、线程池等资源管理都可以采取这种写法。

ServiceLoader 集成

与 ServiceLoader 的集成后续也将非常常用。而且,某些自定义的 Spring bean 也可能会用上延迟加载。从而让 Java 应用的启动速度大幅增加。

public class MyServiceProvider implements MyService {
    private static final LazyConstant<MyServiceProvider> INSTANCE = 
        LazyConstant.of(MyServiceProvider::new);

    // ServiceLoader会调用此方法
    public static MyServiceProvider provider() {
        return INSTANCE.get();
    }
}

性能对比:数据说话

实现方式首次调用开销后续调用开销线程安全常量折叠代码复杂度
饿汉式最低安全支持
双重检查锁定中等安全不支持
静态内部类最低安全支持
LazyConstants极低最低安全支持极低

根据 OpenJDK 官方数据微基准测试结果表明:

  • 相比volatile双重检查锁定,LazyConstants后续调用性能提升 15-30%
  • 启动时未使用的组件,节省初始化时间可达 100%(完全避免)

使用注意事项

根据社区的反馈,整体来说,使用这个特性我总结了 4 条注意事项,供大家参考。

预览特性启用

目前还是一个预览特性,不过后续应该是没啥改动了,预计 JDK 26 或 27 可能会被彻底放开。

# 编译
javac --release 26 --enable-preview Main.java

# 运行
java --enable-preview Main

final字段的重要性

直接看下面的用法吧。

// 正确:能触发常量折叠
static final LazyConstant<Config> CONFIG = LazyConstant.of(...);

// 错误:失去优化效果
static LazyConstant<Config> config = LazyConstant.of(...);

避免在计算函数中抛出异常

这个和写单例一样,不建议在创建单例时直接抛出异常。

// 不推荐
LazyConstant.of(() -> {
    if (somethingWrong) throw new RuntimeException();
    return value;
});

// 推荐
LazyConstant.of(() -> {
    try {
        return computeValue();
    } catch (Exception e) {
        // 提供默认值
        return DEFAULT_VALUE; 
    }
});

限制反射修改

目前实例 final 字段仍可通过反射修改,这会破坏常量折叠。但static final字段是安全的。

总结

JEP 526 标志着 Java 延迟初始化 API 的成熟。未来我们可能看到:

  1. 语言级支持:如lazy final字段声明
  2. 完整性提升:限制反射对 final 字段的修改(JEP 500)
  3. 更广泛的 JVM 优化:基于 Project Valhalla 的值类型集成

Lazy Constants不是简单的 API 重命名,而是一次重要的设计重塑。

  • 更清晰的语义:从底层机制描述转向使用场景描述
  • 更安全的 API:移除易错操作,强制构造时提供计算逻辑
  • 更友好的开发体验:与集合 API 深度集成

对于 Java 开发者而言,这意味着。

  • 单例模式实现更简单:一行代码解决线程安全和性能问题
  • 应用启动更快:按需初始化,避免不必要的开销
  • 代码更易维护:意图明确,减少 boilerplate

未来在项目中采用 LazyConstants 替代传统的双重检查锁定和静态内部类实现,甚至是 Spring bean 的整体延迟初始化实现,享受现代 Java 带来的性能与简洁。

业余草公众号

最后,欢迎关注我的个人微信公众号:业余草(yyucao)!可加作者微信号:xttblog2。备注:“1”,添加博主微信拉你进微信群。备注错误不会同意好友申请。再次感谢您的关注!后续有精彩内容会第一时间发给您!原创文章投稿请发送至532009913@qq.com邮箱。商务合作也可添加作者微信进行联系!

本文原文出处:业余草: » Java 单例模式终于有救!JDK 26 LazyConstants 终结手写线程安全时代