17、JDK 25 新特性:稳定值(JEP 502)性能优化预览

在写高性能应用的时候,咱经常需要延迟初始化一些对象,比如日志记录器、配置信息、缓存啥的。这些对象可能不是一开始就需要,但一旦初始化了就不会变了,而且会被频繁访问。以前咱用 final 字段配合静态初始化,或者用双重检查锁定(Double-Checked Locking)来实现,代码写起来又臭又长,还容易出错。

JDK 25 里的 JEP 502 引入了稳定值(Stable Values)这个预览特性,专门用来解决这个问题。稳定值允许你声明一个值,这个值最多初始化一次,一旦初始化了就不能改了,JVM 会把它当成常量来优化,性能跟 final 字段差不多,但初始化时机更灵活。

这个特性对需要延迟初始化的场景特别有用,比如日志记录器、配置加载、单例模式啥的。JVM 可以对稳定值做常量折叠(Constant Folding)优化,把重复的计算消除掉,性能提升很明显。而且它是线程安全的,不需要手动加锁,用起来比双重检查锁定简单多了。

稳定值是啥

先说说稳定值到底是啥吧。稳定值就是一个不可变的值,它最多被设置一次,一旦设置了就不能改了。JVM 会把稳定值当成常量来优化,就像 final 字段一样,可以做常量折叠、内联啥的优化。

稳定值的核心特点有几个:第一个是延迟初始化,可以在需要的时候才初始化,不用一开始就准备好;第二个是不可变性,一旦设置了就不能改了,保证了线程安全;第三个是 JVM 优化,JVM 会把稳定值当成常量,可以做各种优化;第四个是线程安全,不需要手动加锁,内部已经处理好了。

稳定值跟 final 字段的区别是,final 字段必须在构造的时候初始化,稳定值可以在任何时候初始化,只要还没初始化过就行。稳定值跟 volatile 的区别是,volatile 可以多次修改,稳定值只能设置一次,而且 JVM 优化更好。

稳定值跟懒加载(Lazy Initialization)的区别是,懒加载通常需要手动加锁,稳定值内部已经处理好了线程安全,用起来更简单。而且稳定值可以让 JVM 做常量折叠优化,性能更好。

为啥需要稳定值

以前咱要实现延迟初始化,通常用几种方式:第一种是静态初始化,在类加载的时候就初始化,但这样会拖慢启动速度;第二种是双重检查锁定,用 volatilesynchronized 来实现,代码写起来麻烦,还容易出错;第三种是 ThreadLocal,但它是线程级别的,不适合全局单例。

这些方式都有问题:静态初始化会拖慢启动速度,特别是那些初始化成本高的对象;双重检查锁定代码复杂,容易写错,而且性能也不够好;ThreadLocal 不适合全局单例,而且有内存泄漏的风险。

稳定值就是为了解决这些问题而设计的,它提供了简单、安全、高效的延迟初始化方式。用稳定值,你可以把初始化逻辑写在一个地方,JVM 会自动处理线程安全,而且会做常量折叠优化,性能很好。

基本用法

稳定值的基本用法很简单,先创建一个 StableValue 实例,然后在需要的时候初始化它。看个例子:

import java.lang.StableValue;  // 稳定值在 java.lang 包里
import java.util.logging.Logger;  // 日志记录器

public class OrderService {
    // 创建一个稳定值,用来存日志记录器
    private final StableValue<Logger> logger = StableValue.of();  // 创建一个空的稳定值,还没初始化
    
    // 获取日志记录器,如果还没初始化就初始化它
    public Logger getLogger() {
        // orElseSet 方法:如果值还没设置,就用提供的 Supplier 来设置;如果已经设置了,就返回已有的值
        return logger.orElseSet(() -> Logger.getLogger(OrderService.class.getName()));  // 第一次调用会初始化,后续调用直接返回
    }
    
    // 提交订单的方法
    public void submitOrder(String userId, List<String> products) {
        getLogger().info("开始提交订单,用户: " + userId);  // 第一次调用 getLogger() 会初始化日志记录器
        // 处理订单逻辑...
        getLogger().info("订单提交成功,产品数量: " + products.size());  // 后续调用直接返回已有的日志记录器,JVM 会优化
    }
}

这个例子展示了稳定值的基本用法。StableValue.of() 创建一个空的稳定值,orElseSet() 方法会在值还没设置的时候用提供的 Supplier 来设置它,如果已经设置了就直接返回。

第一次调用 getLogger() 的时候,稳定值还没初始化,orElseSet() 会执行 Supplier,创建日志记录器并设置到稳定值里。后续调用 getLogger() 的时候,稳定值已经初始化了,直接返回已有的值,JVM 会做常量折叠优化,性能很好。

线程安全

稳定值是线程安全的,不需要手动加锁。多个线程同时调用 orElseSet() 的时候,只有一个线程会执行 Supplier,其他线程会等待,然后直接返回已经设置的值。看个例子:

import java.lang.StableValue;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ThreadSafeExample {
    // 创建一个稳定值,用来存配置信息
    private final StableValue<Config> config = StableValue.of();  // 配置对象,延迟初始化
    private volatile int initCount = 0;  // 用来统计初始化次数,应该是 1
    
    // 获取配置,多个线程可能同时调用这个方法
    public Config getConfig() {
        // 多个线程同时调用 orElseSet,只有一个会执行 Supplier
        return config.orElseSet(() -> {
            initCount++;  // 这个应该只会执行一次
            System.out.println("初始化配置,线程: " + Thread.currentThread().getName());  // 打印初始化信息
            return new Config("app.properties");  // 创建配置对象
        });
    }
    
    // 测试多线程访问
    public static void main(String[] args) throws InterruptedException {
        ThreadSafeExample example = new ThreadSafeExample();
        ExecutorService executor = Executors.newFixedThreadPool(10);  // 创建线程池,10 个线程
        CountDownLatch latch = new CountDownLatch(10);  // 用来等待所有线程完成
        
        // 启动 10 个线程,同时调用 getConfig()
        for (int i = 0; i < 10; i++) {
            executor.submit(() -> {
                try {
                    Config config = example.getConfig();  // 多个线程同时获取配置
                    System.out.println("线程 " + Thread.currentThread().getName() + " 获取到配置: " + config);
                } finally {
                    latch.countDown();  // 完成一个线程
                }
            });
        }
        
        latch.await();  // 等待所有线程完成
        executor.shutdown();  // 关闭线程池
        
        // 验证初始化只执行了一次
        System.out.println("初始化次数: " + example.initCount);  // 应该是 1
    }
    
    // 配置类
    static class Config {
        private final String filePath;  // 配置文件路径
        
        public Config(String filePath) {
            this.filePath = filePath;
        }
        
        @Override
        public String toString() {
            return "Config{filePath='" + filePath + "'}";
        }
    }
}

这个例子展示了稳定值的线程安全性。10 个线程同时调用 getConfig(),但 orElseSet() 里的 Supplier 只会执行一次,其他线程会等待,然后直接返回已经设置的值。initCount 应该是 1,说明初始化只执行了一次。

性能优化

稳定值的性能优化主要体现在 JVM 的常量折叠上。一旦稳定值被初始化了,JVM 会把它当成常量,可以做各种优化,比如常量折叠、内联、消除重复计算啥的。

看个性能对比的例子:

import java.lang.StableValue;
import java.util.function.Supplier;

public class PerformanceExample {
    // 用稳定值实现延迟初始化
    private final StableValue<ExpensiveObject> stableValue = StableValue.of();  // 稳定值方式
    
    // 用传统的双重检查锁定实现延迟初始化
    private volatile ExpensiveObject volatileValue;  // volatile 方式
    private final Object lock = new Object();  // 锁对象
    
    // 用稳定值获取对象
    public ExpensiveObject getStableValue() {
        // 稳定值方式,JVM 可以做常量折叠优化
        return stableValue.orElseSet(() -> {
            System.out.println("稳定值初始化");  // 只会打印一次
            return new ExpensiveObject();  // 创建昂贵对象
        });
    }
    
    // 用双重检查锁定获取对象
    public ExpensiveObject getVolatileValue() {
        // 双重检查锁定,代码复杂,性能也不够好
        if (volatileValue == null) {  // 第一次检查,避免不必要的加锁
            synchronized (lock) {  // 加锁
                if (volatileValue == null) {  // 第二次检查,确保只初始化一次
                    System.out.println("volatile 初始化");  // 只会打印一次
                    volatileValue = new ExpensiveObject();  // 创建昂贵对象
                }
            }
        }
        return volatileValue;  // 返回对象
    }
    
    // 性能测试
    public static void main(String[] args) {
        PerformanceExample example = new PerformanceExample();
        
        // 预热
        for (int i = 0; i < 1000; i++) {
            example.getStableValue();  // 预热稳定值方式
            example.getVolatileValue();  // 预热 volatile 方式
        }
        
        // 测试稳定值方式的性能
        long start = System.nanoTime();
        for (int i = 0; i < 10_000_000; i++) {
            ExpensiveObject obj = example.getStableValue();  // 调用稳定值方式
        }
        long stableTime = System.nanoTime() - start;
        
        // 测试 volatile 方式的性能
        start = System.nanoTime();
        for (int i = 0; i < 10_000_000; i++) {
            ExpensiveObject obj = example.getVolatileValue();  // 调用 volatile 方式
        }
        long volatileTime = System.nanoTime() - start;
        
        System.out.println("稳定值方式耗时: " + stableTime / 1_000_000 + " ms");  // 打印稳定值方式耗时
        System.out.println("volatile 方式耗时: " + volatileTime / 1_000_000 + " ms");  // 打印 volatile 方式耗时
        System.out.println("性能提升: " + (volatileTime - stableTime) * 100 / volatileTime + "%");  // 计算性能提升
    }
    
    // 昂贵的对象,模拟初始化成本高的对象
    static class ExpensiveObject {
        private final String data;
        
        public ExpensiveObject() {
            // 模拟昂贵的初始化操作
            StringBuilder sb = new StringBuilder();  // 创建字符串构建器
            for (int i = 0; i < 1000; i++) {
                sb.append("data").append(i);  // 拼接字符串
            }
            this.data = sb.toString();  // 保存数据
        }
    }
}

这个例子对比了稳定值和双重检查锁定的性能。稳定值方式代码更简单,而且 JVM 可以做常量折叠优化,性能通常更好。volatile 方式需要每次检查,而且不能做常量折叠,性能相对差一些。

实际应用场景

稳定值在实际应用中有很多场景,比如日志记录器、配置加载、单例模式、缓存啥的。看几个实际应用的例子:

日志记录器

日志记录器是最常见的应用场景,因为不是所有代码路径都会用到日志,延迟初始化可以提升启动速度:

import java.lang.StableValue;
import java.util.logging.Logger;

public class UserService {
    // 用稳定值存日志记录器,延迟初始化
    private final StableValue<Logger> logger = StableValue.of();
    
    // 获取日志记录器
    private Logger getLogger() {
        return logger.orElseSet(() -> Logger.getLogger(UserService.class.getName()));  // 第一次调用才初始化
    }
    
    // 创建用户
    public void createUser(String username, String email) {
        getLogger().info("开始创建用户: " + username);  // 如果这个方法没被调用,日志记录器就不会初始化
        // 创建用户逻辑...
        getLogger().info("用户创建成功: " + email);  // 后续调用直接返回,JVM 会优化
    }
    
    // 删除用户
    public void deleteUser(String username) {
        getLogger().warning("删除用户: " + username);  // 日志记录器已经初始化了,直接使用
        // 删除用户逻辑...
    }
}

这个例子展示了用稳定值实现日志记录器的延迟初始化。如果 createUser()deleteUser() 没被调用,日志记录器就不会初始化,可以提升启动速度。一旦初始化了,后续调用就直接返回,JVM 会做常量折叠优化。

配置加载

配置信息通常需要从文件或数据库加载,延迟初始化可以避免启动时加载不必要的配置:

import java.lang.StableValue;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Properties;

public class AppConfig {
    // 用稳定值存配置信息,延迟加载
    private final StableValue<Properties> config = StableValue.of();
    
    // 获取配置
    public Properties getConfig() {
        return config.orElseSet(() -> {
            Properties props = new Properties();  // 创建配置对象
            try {
                // 从文件加载配置,这个操作可能比较慢
                props.load(Files.newInputStream(Paths.get("app.properties")));  // 加载配置文件
                System.out.println("配置加载完成");  // 打印加载信息
            } catch (IOException e) {
                throw new RuntimeException("加载配置失败", e);  // 加载失败抛异常
            }
            return props;  // 返回配置对象
        });
    }
    
    // 获取配置项
    public String getProperty(String key) {
        return getConfig().getProperty(key);  // 第一次调用会加载配置,后续调用直接返回
    }
    
    // 获取配置项,带默认值
    public String getProperty(String key, String defaultValue) {
        return getConfig().getProperty(key, defaultValue);  // 配置已经加载了,直接获取
    }
}

这个例子展示了用稳定值实现配置的延迟加载。第一次调用 getConfig() 的时候会从文件加载配置,后续调用直接返回已经加载的配置,JVM 会做常量折叠优化。

单例模式

稳定值可以用来实现线程安全的单例模式,代码比双重检查锁定简单多了:

import java.lang.StableValue;

public class DatabaseConnection {
    // 用稳定值实现单例,线程安全,不需要手动加锁
    private static final StableValue<DatabaseConnection> instance = StableValue.of();
    
    private final String url;  // 数据库 URL
    private final String username;  // 用户名
    
    // 私有构造函数,防止外部创建实例
    private DatabaseConnection(String url, String username) {
        this.url = url;  // 保存 URL
        this.username = username;  // 保存用户名
        System.out.println("数据库连接创建: " + url);  // 打印创建信息
    }
    
    // 获取单例实例
    public static DatabaseConnection getInstance() {
        // 稳定值保证线程安全,而且 JVM 会优化
        return instance.orElseSet(() -> new DatabaseConnection("jdbc:mysql://localhost:3306/mydb", "admin"));  // 第一次调用才创建
    }
    
    // 执行查询
    public void executeQuery(String sql) {
        System.out.println("执行查询: " + sql + " on " + url);  // 打印查询信息
        // 执行查询逻辑...
    }
    
    // 测试
    public static void main(String[] args) {
        // 多个线程同时获取单例,只会创建一个实例
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                DatabaseConnection conn = DatabaseConnection.getInstance();  // 获取单例
                conn.executeQuery("SELECT * FROM users");  // 执行查询
            }).start();  // 启动线程
        }
    }
}

这个例子展示了用稳定值实现线程安全的单例模式。代码比双重检查锁定简单多了,而且 JVM 会做常量折叠优化,性能也很好。

缓存

稳定值可以用来实现简单的缓存,延迟加载数据:

import java.lang.StableValue;
import java.util.HashMap;
import java.util.Map;

public class DataCache {
    // 用稳定值存缓存数据,延迟加载
    private final StableValue<Map<String, String>> cache = StableValue.of();
    
    // 获取缓存
    private Map<String, String> getCache() {
        return cache.orElseSet(() -> {
            System.out.println("初始化缓存");  // 打印初始化信息
            Map<String, String> data = new HashMap<>();  // 创建缓存映射
            // 模拟从数据库加载数据
            data.put("user:1", "Alice");  // 加载用户数据
            data.put("user:2", "Bob");  // 加载用户数据
            data.put("user:3", "Charlie");  // 加载用户数据
            return data;  // 返回缓存
        });
    }
    
    // 获取缓存项
    public String get(String key) {
        return getCache().get(key);  // 第一次调用会初始化缓存,后续调用直接返回
    }
    
    // 检查缓存是否包含某个键
    public boolean containsKey(String key) {
        return getCache().containsKey(key);  // 缓存已经初始化了,直接检查
    }
}

这个例子展示了用稳定值实现简单的缓存。第一次调用 getCache() 的时候会初始化缓存,后续调用直接返回已经初始化的缓存,JVM 会做常量折叠优化。

与 final 字段的对比

稳定值跟 final 字段的区别主要是初始化时机。final 字段必须在构造的时候初始化,稳定值可以在任何时候初始化,只要还没初始化过就行。

看个对比的例子:

import java.lang.StableValue;

public class FinalVsStable {
    // 用 final 字段,必须在构造的时候初始化
    private final String finalValue;
    
    // 用稳定值,可以在任何时候初始化
    private final StableValue<String> stableValue = StableValue.of();
    
    public FinalVsStable(boolean needValue) {
        // final 字段必须在构造的时候初始化
        if (needValue) {
            this.finalValue = "initialized";  // 必须初始化
        } else {
            this.finalValue = null;  // 即使不需要,也得初始化
        }
    }
    
    // 稳定值可以在需要的时候才初始化
    public String getStableValue() {
        return stableValue.orElseSet(() -> {
            // 只有在需要的时候才初始化,可以避免不必要的计算
            System.out.println("稳定值初始化");  // 打印初始化信息
            return "initialized";  // 返回初始化的值
        });
    }
    
    // 如果不需要值,final 字段也会初始化(浪费)
    public String getFinalValue() {
        return finalValue;  // 即使不需要,也已经初始化了
    }
}

这个例子展示了稳定值和 final 字段的区别。final 字段必须在构造的时候初始化,即使不需要也会初始化,可能浪费资源。稳定值可以在需要的时候才初始化,更灵活。

编译和运行

稳定值是预览特性,编译和运行的时候需要启用预览特性。看个例子:

# 编译的时候启用预览特性
javac --release 25 --enable-preview StableValueExample.java

# 运行的时候也要启用预览特性
java --enable-preview StableValueExample

如果用 Maven,可以在 pom.xml 里配置:

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>3.11.0</version>
            <configuration>
                <release>25</release>
                <compilerArgs>
                    <arg>--enable-preview</arg>
                </compilerArgs>
            </configuration>
        </plugin>
    </plugins>
</build>

如果用 Gradle,可以在 build.gradle 里配置:

tasks.withType(JavaCompile) {
    options.compilerArgs += '--enable-preview'
    options.release = 25
}

tasks.withType(JavaExec) {
    jvmArgs += '--enable-preview'
}

注意事项

用稳定值的时候有几个注意事项:

第一个是只能设置一次,一旦设置了就不能改了。如果你需要可变的值,应该用 volatile 或其他方式。

第二个是初始化成本,虽然稳定值支持延迟初始化,但如果初始化成本很高,而且这个值可能不会被用到,延迟初始化才有意义。如果初始化成本很低,或者这个值一定会被用到,直接用 final 字段可能更简单。

第三个是 JVM 优化,稳定值的性能优化依赖于 JVM 的常量折叠,如果 JVM 优化不够好,性能可能不如预期。不过一般来说,JVM 的优化还是很好的。

第四个是预览特性,稳定值在 JDK 25 里是预览特性,API 可能会变。根据 JEP 文档,稳定值在 JDK 26 里可能会改名为 LazyConstant,用的时候要注意版本兼容性。如果你现在用稳定值,等 JDK 26 出来的时候可能需要改代码,不过应该只是改个名字,逻辑不会变。

第五个是空值处理,稳定值可以存 null,但如果你用 orElseSet() 返回 null,后续调用还是会继续尝试初始化。如果你需要区分"还没初始化"和"初始化为 null"这两种情况,可能需要用其他方式,比如用 Optional 包装一下。

最佳实践

用稳定值的时候,有几个最佳实践:

第一个是选择合适的场景,稳定值适合延迟初始化不可变对象的场景,比如日志记录器、配置信息、单例模式啥的。如果需要可变的值,或者初始化成本很低,可能不适合用稳定值。

第二个是避免在循环里初始化,虽然稳定值是线程安全的,但在循环里频繁调用 orElseSet() 可能影响性能。应该先初始化,然后在循环里直接使用。

第三个是配合虚拟线程使用,稳定值配合虚拟线程用的时候效果特别好,因为虚拟线程是轻量级的,延迟初始化可以提升启动速度。

第四个是注意异常处理,如果 orElseSet() 里的 Supplier 抛异常,稳定值不会被设置,后续调用会继续尝试初始化。应该确保 Supplier 不会抛异常,或者在 Supplier 里处理好异常。如果初始化可能失败,可以在 Supplier 里返回一个默认值,或者用 Optional 包装一下,避免后续调用一直失败。

第五个是配合其他特性使用,稳定值可以配合作用域值(Scoped Values)、结构化并发(Structured Concurrency)这些特性一起用,效果会更好。比如在结构化并发的任务里用稳定值存一些共享的配置信息,可以避免重复初始化,提升性能。

总结

稳定值(JEP 502)是 JDK 25 里的预览特性,提供了简单、安全、高效的延迟初始化方式。它允许你声明一个值,这个值最多初始化一次,一旦初始化了就不能改了,JVM 会把它当成常量来优化,性能跟 final 字段差不多,但初始化时机更灵活。

稳定值适合延迟初始化不可变对象的场景,比如日志记录器、配置信息、单例模式、缓存啥的。它是线程安全的,不需要手动加锁,代码比双重检查锁定简单多了。而且 JVM 会做常量折叠优化,性能很好。

不过稳定值是预览特性,API 可能会变,用的时候要注意版本兼容性。而且它只适合不可变对象,如果需要可变的值,应该用其他方式。

总的来说,稳定值是个很实用的特性,特别是对需要延迟初始化的场景,能简化代码,提升性能。如果你在写高性能应用,需要延迟初始化一些对象,可以试试稳定值,应该会有不错的体验。

本文章最后更新于 2025-11-27