ZealSinger头像
JDK27升后LazyConstant怎么用封面图

JDK27升后LazyConstant怎么用

JDK27升后LazyConstant怎么用

升到 JDK 27 后,JEP 531(Lazy Constants,Third Preview)随版本交付(Status Closed/Delivered,Release=27;relnotes 亦收录)。一句话先钉死:它仍是 preview API,编译和运行都要 --enable-preview;不要把「Closed/Delivered」理解成「可当正式 API、可免开关」。

现场更常见的坑是另一类:组件里 Logger、仓库、连接工厂一类字段用 final 急初始化,启动路径被拖慢;改成可变字段又丢常量折叠,还得自己写双重检查。本篇只谈一件事:在 JDK 27 上怎么用 LazyConstant、和 final 差在哪、怎么验、preview 边界在哪。同系列结构化并发另见 《JDK27升后结构化并发preview怎么写》;JFR 脱敏、紧凑对象头各有专篇(167650309、167451590),本稿不抢那几条主线。

痛点:final 急初始化拖启动,手写懒加载又易翻车

三类常见混乱:

  1. final 必须在构造/类初始化期赋值——实例一建出来,Logger.create(...)、读配置、建连接池就全跑完了,哪怕本请求根本用不到。
  2. 改成可变字段手写懒加载——synchronized 一把大锁,或 double-check 漏 volatile;多线程下可能建多个实例,副作用(打日志、占端口、写文件)重复发生。
  3. 丢掉常量折叠——字段不再是 final,JIT 很难把后续读取当成「永远不变」做折叠;为了懒加载,把性能优化也一起丢了。

理想状态是:内容至多算一次、算完不可变、字段仍可走常量信任链,且首次访问才触发。java.lang.LazyConstant 就是冲着这件事来的。

字段不可变内容:何时初始化?

边界:JEP 531 第三预览,仍须开 preview

口径先钉死(openjdk.org/jeps/531;Oracle JDK 27 lazy-constants 文档 Note;SE 27 LazyConstant javadoc):

  • 状态:Closed / Delivered,Release = 27;History 写明「第三预览」(Third Preview)。
  • 性质:preview API,默认关闭。
  • 开关:javac --release 27 --enable-preview … 与 java --enable-preview … 两边都要开。注意:openjdk.org/jeps/531 页示例若仍写 --release 26,那是文稿遗留——对 JDK 27 写 27,不要照抄。
  • 现行类名:java.lang.LazyConstant。JDK 26(JEP 526)曾把 API 从 StableValue 重命名过来;正文里历史一句带过即可,勿把 StableValue 当 27 现行 API。
  • 勿混下一轮:JEP 547(Fourth Preview)目前是 Candidate,文中写拟在 JDK 28 再预览且 无 API 变更——那是后续提案,不要写成 27 已交付行为。27 交付边界以 JEP 531 + SE 27 javadoc 为准。
# 编译(两边都要带 --enable-preview)
javac --release 27 --enable-preview Component.java

# 运行
java --enable-preview Component

为什么必须开 --enable-preview?

因为官方就这么定的。 Closed/Delivered 只表示该 JEP 随 27 进了发行版;交付物本身仍是第三轮预览。javac 开了、java 没开,或反过来,都会在一边过、一边挂。不要暗示「27 起默认可免开关」。

LazyConstant 和 final 字段有什么差别?

维度final Tfinal LazyConstant<T>
赋值时机构造/类初始化必须完成of(Supplier) 时未初始化;首次 get() 才算
内容可变性引用不可变;对象本身另说初始化后内容不可再改(holder 语义)
可否为 null可以(视业务)不可:计算返回 null → 错误态
并发首次访问无「算几次」问题(已急初始化)多线程竞态时至多一次计算
常量折叠经典路径存于 final 字段(及文档所述信任链)时,JVM 可对已初始化内容做折叠

一句话:final 管的是「引用何时钉死」;LazyConstant 管的是「内容何时算、算完钉死」。把 holder 本身放在 final 字段里,两边的好处可以叠在一起。

LazyConstant.of 和 get 怎么写?

官方最小形态(SE 27 javadoc 示例口径):

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

public class Component {

    // 创建时未初始化;字段宜 final,便于常量折叠信任链
    private final LazyConstant<Logger> logger =
            LazyConstant.of(() -> Logger.getLogger(Component.class.getName()));

    public void process() {
        logger.get().info("Process started");
        // ...
    }

    public static void main(String[] args) {
        new Component().process();
    }
}

要点拆开看:

  • LazyConstant.of(Supplier):返回未初始化的 holder;传入的计算函数此时不会执行。
  • get():首次调用时执行计算函数,用结果初始化;之后永远返回同一内容。
  • 字段写成 final:文档 Performance 节写明——常量折叠依赖从 static final(或经信任链:static final / record 字段 / hidden class 的 final 实例字段)直达 lazy constant。实例字段场景也建议 final,把「holder 不可替换」钉死。
  • 内容不可为 null:需要可空语义时,用 Optional 包一层,而不是让 Supplier 返回 null。

组合:一个 LazyConstant 依赖另一个

import java.lang.LazyConstant;

public final class Graph {

    public static final class Foo {
        // ...
    }

    public static final class Bar {
        public Bar(Foo foo) {
            // ...
        }
    }

    static final LazyConstant<Foo> FOO = LazyConstant.of(Foo::new);
    static final LazyConstant<Bar> BAR = LazyConstant.of(() -> new Bar(FOO.get()));

    public static Foo foo() {
        return FOO.get();
    }

    public static Bar bar() {
        return BAR.get();
    }

    public static void main(String[] args) {
        // 首次 bar():若 FOO 未就绪,会先算 FOO,再算 BAR
        System.out.println(bar());
    }
}

调用 BAR.get() 时,若依赖的 FOO 还没初始化,会先算 FOO。形成依赖图可以,但别搞成环:计算函数经同一 holder 递归自调用,会进错误态(见下节)。

集合侧还有 List.ofLazy / Map.ofLazy / Set.ofLazy(均为 27 preview;其中 Set.ofLazy 相对 26 是增量)——适合「按索引 / 按键 / 按候选」惰性填槽,本稿不展开成长文。

多线程同时 get 会算几次?

至多一次。 Thread Safety 节明文:竞态时只有一个线程跑计算函数(在调用方线程上执行),其余线程阻塞到初始化完成(或失败进入错误态);胜者更新后,其它线程观察已初始化(或错误态),不会再算一遍。初始化与内容发布之间有 happens-before,新对象上的 final 字段可安全发布。

注意边界:

  • 中断不取消初始化:计算线程被 interrupt,get() 不会清中断状态,也不会替你抛 InterruptedException。
  • 无限阻塞无超时:计算函数若一直堵着,其它在 get() 上等的线程也会一直堵——没有内建 timeout / cancel。

多线程 get:至多一次计算

计算失败或返回 null 会怎样?

失败路径按 javadoc「Exception handling」对齐,不要凭印象写:

  1. 计算函数抛未检查异常(运行时异常或 Error)→ holder 不初始化,转入错误态,本次 get() 抛 NoSuchElementException,cause 为原异常。
  2. 之后再 get() → 仍抛 NoSuchElementException,不再调用计算函数;后续调用无 cause,message 里会带上原异常的类名。
  3. 返回 null → 同样进错误态;首次 NoSuchElementException 的 cause 为 NullPointerException。
  4. 计算函数经同一 lazy constant 递归自调用 → 首次 NoSuchElementException 的 cause 为 IllegalStateException。
import java.lang.LazyConstant;
import java.util.NoSuchElementException;
import java.util.concurrent.atomic.AtomicInteger;

public class ErrorStateDemo {

    static final AtomicInteger attempts = new AtomicInteger();

    static final LazyConstant<String> BAD = LazyConstant.of(() -> {
        attempts.incrementAndGet();
        throw new IllegalArgumentException("boom");
    });

    static final LazyConstant<String> NULLISH = LazyConstant.of(() -> {
        attempts.incrementAndGet();
        return null; // 禁止:会进错误态
    });

    public static void main(String[] args) {
        try {
            BAD.get();
        } catch (NoSuchElementException ex) {
            // 首次:cause 为 IllegalArgumentException
            System.out.println("1st cause=" + ex.getCause());
        }
        try {
            BAD.get();
        } catch (NoSuchElementException ex) {
            // 后续:无 cause;message 含原异常类名
            System.out.println("2nd cause=" + ex.getCause()
                    + ", msg=" + ex.getMessage());
        }
        System.out.println("attempts for BAD path observed via separate constant");

        try {
            NULLISH.get();
        } catch (NoSuchElementException ex) {
            // cause 为 NullPointerException
            System.out.println("null path cause=" + ex.getCause());
        }
    }
}

验收时盯两件事:错误态不可恢复(不会「下次再试一次」),以及 null 与抛异常走同一类错误态语义。业务上若需要可空,用 Optional;若需要可重试,不要指望同一个 LazyConstant 实例帮你重算——换工厂、换实例,或根本别用惰性常量表达「可失败可重试」的流程。

怎么验:单线程一次、并发一次、错误态、preview 开关

建议按四步过,每步只验一件事:

  1. Preview 开关
    去掉 --enable-preview 应无法正常使用该 preview API;javac / java 两侧都带上才能跑通上文 Component。

  2. 单线程只算一次
    在 Supplier 里打计数器或副作用日志;连续 get() 两次,计数应为 1,两次引用相同(==)。

  3. 两线程并发仍只算一次
    两个线程同时 get(),计数仍为 1;两边拿到同一实例。可对照上面的时序图。

  4. 故意失败 / 返回 null
    看首次 NoSuchElementException 的 cause,再看第二次无 cause、且计数不再增加。

可选对照:同一场景改回手写 synchronized 懒加载,观察锁竞争与「忘了 volatile」时的风险——用来理解「为什么要换成 LazyConstant」,不是上线必跑项。

边界再钉几句

  • Preview 不是正式 API:Closed/Delivered ≠ 可免 --enable-preview;生产是否采用,按「是否真有延后初始化收益」评估,并接受后续预览轮次仍可能改签名。
  • 字段宜 final:常量折叠信任链写在文档里;别把 holder 放进普通可变字段还指望同等优化。
  • 强引用内容:初始化后内容不会被 holder 丢掉;可达的 lazy constant 会让内容一直可达——长期缓存大对象时留意泄漏面。
  • 不可序列化:LazyConstant 不是 Serializable。
  • 静态初始化顺序:用在静态初始化器里可能与类初始化顺序交互;环状初始化按 JLS §12.4 可能直接炸——别用它硬拧启动环。
  • 同系列别文:结构化并发、JFR 脱敏、紧凑对象头各有专篇(文首内链),本文不抢那些主线。

选型时别踩的两个坑

第一,把「交付」当成「可免 preview」。Closed/Delivered 只说明 JEP 531 随 JDK 27 进了发行版,并不取消 preview 开关,也不等于下一轮(JEP 547 / 拟 28)可以无视。第二,用 LazyConstant 表达可重试流程。错误态是终态;需要重试、需要可空、需要可替换实现,换别的工具。先把 of / get、并发至多一次、失败进错误态三条跑通,再决定哪些 final 急初始化值得迁。

小结

项口径
交付JEP 531,Third Preview,Closed/Delivered @ JDK 27
开关javac / java 都要 --enable-preview(--release 27,勿抄 JEP 页遗留的 26)
写法final LazyConstant<T> x = LazyConstant.of(supplier); → x.get()
并发多线程 get 至多一次计算
失败未检查异常 / null / 递归自调用 → 错误态 → NoSuchElementException(cause 规则见 javadoc)
集合List/Map/Set.ofLazy 均为 27 preview;Set.ofLazy 为相对 26 增量
旧名StableValue 仅 26 历史;现行名 LazyConstant
勿混JEP 547(拟 28、无 API 变更)≠ 27 已交付行为

先把 preview 开关和最小 of / get 跑通,再验并发与错误态;集合惰性工厂放到第二步。它依旧是 preview——写对、验清,比急着全仓替换 final 更重要。

转载自 CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/Amax3747/article/details/168308773

文章来源转载

评论

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--