Uploaded image for project: 'JDK'
  1. JDK
  2. JDK-8359894

JEP 526: Lazy Constants (Second Preview)

XMLWordPrintable

    • Per Minborg & Maurizio Cimadamore
    • Feature
    • Open
    • SE
    • core dash libs dash dev at openjdk dot org
    • S
    • S
    • 526

      Summary

      Introduce an API for lazy constants, which are objects that hold immutable data. Lazy constants are treated as true constants by the JVM, enabling the same performance optimizations that are enabled by declaring a field final. Compared to final fields, however, lazy constants offer greater flexibility as to the timing of their initialization. This is a preview API.

      History

      This API first previewed in JDK 25 via JEP 502: Stable Values. Based on experience and feedback, we here propose to revise and re-preview the API in JDK 26. Specifically, we propose to:

      Goals

      • Enable application state to be initialized incrementally, on demand, rather than monolithically, thereby reducing application startup time.

      • Decouple the creation of lazy constants from their initialization, without significant performance penalties.

      • Guarantee that lazy constants are initialized at most once, even in multi-threaded programs.

      • Enable user code to benefit from constant-folding optimizations previously applicable only to JDK-internal code.

      Non-goals

      • It is not a goal to enhance the Java programming language with a means to declare lazy fields.

      • It is not a goal to alter the semantics of final fields.

      Motivation

      Most Java developers have heard the advice to "prefer immutability" or "minimize mutability" (Effective Java, Third Edition, Item 17). Immutability confers many advantages, since an immutable object can be in only one state and therefore can be shared freely across multiple threads.

      The Java Platform’s main tool for managing immutability is final fields. Unfortunately, final fields have restrictions. They must be set eagerly, either during construction for instance fields or during class initialization for static fields. Moreover, the order in which final fields are initialized is determined by the textual order in which the fields are declared. Such limitations restrict the applicability of final in many real-world applications.

      Immutability in practice

      Consider a simple application component that records events via a logger object:

      class OrderController {
      
          private final Logger logger = Logger.create(OrderController.class);
      
          void submitOrder(User user, List<Product> products) {
              logger.info("order started");
              ...
              logger.info("order submitted");
          }
      
      }

      Since logger is a final field of the OrderController class, this field must be initialized eagerly, whenever an instance of OrderController is created. This means that creating a new OrderController can be slow — after all, obtaining a logger sometimes entails expensive operations such as reading and parsing configuration data, or preparing the storage where logging events will be recorded.

      Furthermore, if an application is composed of not just one component with a logger but several components, then the application as a whole will start slowly because each and every component will eagerly initialize its logger:

      class Application {
          static final OrderController   ORDERS   = new OrderController();
          static final ProductRepository PRODUCTS = new ProductRepository();
          static final UserService       USERS    = new UserService();
      }

      This initialization work is not only detrimental to the application’s startup, but it might not be necessary. After all, some components might never need to log an event, so why do all this expensive work up front?

      Embracing mutability for more flexible initialization

      For these reasons, we often delay the initialization of complex objects to as late a time as possible, so that they are created only when needed. One way to achieve this is to abandon final and rely on mutable fields to express at-most-once mutation:

      class OrderController {
      
          private Logger logger = null;
      
          Logger getLogger() {
              if (logger == null) {
                  logger = Logger.create(OrderController.class);
              }
              return logger;
          }
      
          void submitOrder(User user, List<Product> products) {
              getLogger().info("order started");
              ...
              getLogger().info("order submitted");
          }
      
      }

      Since logger is no longer a final field, we can move its initialization into the getLogger method. This method checks whether a logger object already exists; if not, it creates a new logger object and stores that in the logger field. While this approach improves application startup, it has some drawbacks:

      • The code has a subtle new invariant: All accesses to the logger field in OrderController must be mediated via the getLogger method. Failure to respect this invariant might expose a not-yet-initialized field, which will result in a NullPointerException.

      • Relying on mutable fields raises correctness and efficiency issues if the application is multi-threaded. For example, concurrent calls to the submitOrder method could result in multiple logger objects being created; even if doing so is correct, it is likely not efficient.

      • One might expect the JVM to optimize access to the logger field by, e.g., constant-folding access to an already-initialized logger field, or by eliding the logger == null check in the getLogger method. Unfortunately, since the field is no longer final, the JVM cannot trust its content never to change after its initial update. Flexible initialization implemented with mutable fields is not efficient.

      Toward deferred immutability

      In a nutshell, the ways in which the Java language allows us to control field initialization are either too constrained or too unconstrained. On the one hand, final fields are too constrained, requiring initialization to occur early in the lifetime of an object or a class, which often degrades application startup. On the other hand, flexible initialization via the use of mutable non-final fields makes it more difficult to reason about correctness. The tension between immutability and flexibility leads developers to adopt imperfect techniques that do not address the fundamental problem and result in code that is even more brittle and difficult to maintain. (Further examples are shown below.)

      What we are missing is a way to promise that a field will be initialized by the time it is used, with a value that is computed at most once and, furthermore, safely with respect to concurrency. In other words, we need a way to defer immutability. This would give the Java runtime broad flexibility in scheduling and optimizing the initialization of such fields, avoiding the penalties that plague the alternatives. First-class support for deferred immutability would fill an important gap between immutable and mutable fields.

      Description

      A lazy constant is an object, of type [<code class="prettyprint" >java.lang.LazyConstant</code>], that holds a single data value, its content. A lazy constant must be initialized some time before its content is first retrieved. This is done using a computing function, typically a lambda expression or a method reference, provided at construction. After a lazy constant is initialized, it is thereafter immutable. A lazy constant is a way to achieve deferred immutability.

      Here is the OrderController class, rewritten to use a lazy constant for its logger:

      class OrderController {
      
          // OLD:
          // private Logger logger = null;
      
          // NEW:
          private final LazyConstant<Logger> logger
              = LazyConstant.of(() -> Logger.create(OrderController.class));
      
          void submitOrder(User user, List<Product> products) {
              logger.get().info("order started");
              ...
              logger.get().info("order submitted");
          }
      
      }

      The logger field holds a lazy constant, created with the static factory method LazyConstant.of(...). Initially, the lazy constant is uninitialized, i.e., it holds no content.

      The submitOrder method calls logger.get() to retrieve a logger. If the lazy constant was already initialized, then the get method returns the content. If the lazy constant is uninitialized, then the get method initializes its content with the value returned by invoking the lambda expression provided at construction, causing the lazy constant to become initialized; the method then returns that value. The get method thus guarantees that a lazy constant is initialized before it returns.

      Even though the lazy constant, once initialized, is immutable, we are not forced to initialize its content in a constructor or, for a static value, in a class initializer. Rather, we initialize it on demand. Furthermore, the get method guarantees that the provided lambda expression is evaluated only once, even when logger.get() is invoked concurrently. This property is crucial, since the evaluation of the lambda expression may have side effects; e.g., the call to Logger.create(...) might create a new file in the filesystem.

      This is a preview API, disabled by default

      To use the Lazy Constants API, you must enable preview features:

      • Compile your program with javac --release 26 --enable-preview Main.java, and run it with java --enable-preview Main; or,

      • When using the source code launcher, run your program with java --enable-preview Main.java; or

      • When using jshell, start it with jshell --enable-preview.

      Flexible initialization with lazy constants

      Lazy constants give us the same guaranteed initialization as immutable final fields, while retaining the flexibility of mutable non-final fields. They therefore fill the gap between these two kinds of fields:

      .storage-kinds { TH { vertical-align: bottom; } TH, TD { padding-top: 1ex; } TD + TD, TH + TH { padding-left: 2em; } TD:first-child { white-space: nowrap; } }

      Update count Update location Constant folding? Concurrent updates? final field 1 Constructor or static initializer Yes No LazyConstant [0, 1] Anywhere Yes, after initialization Yes, by winner Non-final field [0, ∞) Anywhere No Yes

      The flexibility of lazy constants enables us to re-imagine the initialization of entire applications. In particular, we can compose lazy constants from other lazy constants. Just as we used a lazy constant to store the logger in the OrderController component, we can also use a lazy constant to store the OrderController component itself, and related components:

      class Application {
      
          // OLD:
          // static final OrderController   ORDERS   = new OrderController();
          // static final ProductRepository PRODUCTS = new ProductRepository();
          // static final UserService       USERS    = new UserService();
      
          // NEW:
          static final LazyConstant<OrderController>   ORDERS   = LazyConstant.of(OrderController::new);
          static final LazyConstant<ProductRepository> PRODUCTS = LazyConstant.of(ProductRepository::new);
          static final LazyConstant<UserService>       USERS    = LazyConstant.of(UserService::new);
      
          public static OrderController orders() {
              return ORDERS.get();
          }
      
          public static ProductRepository products() {
              return PRODUCTS.get();
          }
      
          public static UserService users() {
              return USERS.get();
          }
      
      }

      The application's startup time improves because it no longer initializes its components, such as OrderController, up front. Rather, it initializes each component on demand, via the computing function and the get method of the corresponding lazy constant. Each component, moreover, initializes its sub-components, such as its logger, on demand in the same way.

      There is, furthermore, mechanical sympathy between lazy constants and the Java runtime. Under the hood, the content of a lazy constant is stored in a non-final field annotated with the JDK-internal [<code class="prettyprint" >@Stable</code>] annotation. This annotation is a common feature of low-level JDK code. It asserts that, even though the field is non-final, the field’s value will not change after the field’s initial and only update. This allows the JVM to treat the content of a lazy constant as a true constant, provided that the field which refers to the lazy constant is final. Thus the JVM can apply constant-folding optimizations to code that accesses immutable data through multiple levels of lazy constants, e.g., Application.orders().getLogger().

      Consequently, developers no longer have to choose between flexible initialization and peak performance.

      Aggregating lazy constants

      Many applications use collections whose elements are themselves deferred immutable data, sharing similar initialization logic.

      Consider, e.g., an application that creates not a single OrderController but a pool of such objects. Different application requests can be served by different OrderController objects, sharing the load across the pool. Objects in the pool should not be created eagerly, but only when a new object is needed by the application. We can achieve this using a lazy list:

      class Application {
      
          // OLD:
          // static final OrderController ORDERS = new OrderController();
      
          // NEW:
          static final List<OrderController> ORDERS
              = List.ofLazy(POOL_SIZE, _ -> new OrderController());
      
          public static OrderController orders() {
              long index = Thread.currentThread().threadId() % POOL_SIZE;
              return ORDERS.get((int)index);
          }
      
      }

      Here ORDERS is no longer a lazy constant but, rather, a lazy list, i.e., a [<code class="prettyprint" >List</code>] in which each element is stored in a lazy constant. When a lazy list is created, via <code class="prettyprint" data-shared-secret="1758996814339-0.3099695105533423">List.ofLazy(...)</code>, its size is fixed, in this case to POOL_SIZE. The lazy constants in which the list elements are stored are not yet initialized.

      To obtain an OrderController, the orders methods calls ORDERS.get(...), passing it an index computed from the current thread's numeric identifier, rather than ORDERS.get(). The first invocation of ORDERS.get(...) with a particular index invokes the computing function provided at construction, in this case a lambda that ignores the index and invokes the OrderController constructor. It uses the resulting OrderController object to initialize the content of the indexed element's underlying lazy constant, and then returns that object. Subsequent invocations of ORDERS.get(...) with the same index return the element's content immediately.

      The elements of a lazy list are initialized independently, as they are needed. For example, if the application runs in a single thread then only one OrderController will ever be created and added to ORDERS.

      Lazy lists retain many of the benefits of lazy constants. The computing function used to initialize the list's elements is evaluated only once per element, even if the lazy list is accessed concurrently. The JVM can, as usual, apply constant-folding optimizations to code that accesses the content of lazy constants through lazy lists.

      Alternatively, we could solve the same problem in a different way with a lazy map, i.e., a Map whose keys are known at construction and whose values are stored in lazy constants, initialized on demand by a computing function that is also provided at construction:

      class Application {
      
          // NEW:
          static final Map<String, OrderController> ORDERS
              = Map.ofLazy(Set.of("Customers", "Internal", "Testing"),
                           _ -> new OrderController());
      
          public static OrderController orders() {
              String threadName = Thread.currentThread().getName();
              return ORDERS.get(threadName);
          }
      
      }

      In this example, OrderController instances are associated with thread names, i.e., "Customers", "Internal", and "Testing", rather than integer indexes computed from thread identifiers. Lazy maps allow for more expressive access idioms than lazy lists, but otherwise have all the same benefits.

      Future Work

      Lazy constants cover the common, high-level use cases for lazy initialization. In the future we might consider providing stable access semantics directly, at a lower level, for reference, array, and primitive fields. This would address, for example, use cases where the computing function associated with a lazy constant is not known at construction.

      Alternatives

      There are many ways to express deferred immutability in Java code today. Unfortunately, the known techniques come with disadvantages that include limited applicability, increased startup cost, and the hindrance of constant-folding optimizations.

      Class-holder idiom

      A common technique is the so-called

      class-holder idiom

      . The class-holder idiom ensures deferred immutability with at-most-once semantics by leveraging the laziness of the JVM's class initialization process:

      class OrderController {
      
          public static Logger getLogger() {
      
              class Holder {
                  private static final Logger LOGGER = Logger.create(...);
              }
      
              return Holder.LOGGER;
          }
      }

      While this idiom allows constant-folding optimizations, it is only applicable to static fields. Moreover, if several fields are to be handled then a separate holder class is required for each field; this makes applications harder to read, slower to start up, and consume more memory.

      Double-checked locking

      Another alternative is the double-checked locking idiom. The basic idea here is to use a fast path to access a variable's value after it has been initialized, and a slow path in the assumed-rare case that a variable's value appears unset:

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

      Since logger is a mutable field, constant-folding optimizations cannot be applied here. More importantly, for the double-checked idiom to work, the logger field must be declared volatile. This guarantees that the field's value is read and updated consistently across multiple threads.

      Double-checked locking on arrays

      Implementing a double-checked locking construct capable of supporting arrays of deferred immutable values is more difficult, since there is no way to declare an array whose elements are volatile. Instead, a client must arrange for volatile access to array elements explicitly, using a VarHandle object:

      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 is needed here to guarantee we only // see fully initialized element objects Logger v = (Logger)LOGGERS_HANDLE.getVolatile(loggers, index); if (v == null) { // Use distinct mutex objects for each index synchronized (mutexes[index]) { // A plain read suffices here since updates to an element // always take place under the same mutex as for this read v = loggers[index]; if (v == null) { // Volatile is needed here to establish a happens-before // relation with future volatile reads LOGGERS_HANDLE.setVolatile(loggers, index, v = Logger.create(... index ...)); } } } return v; } }

      This code is convoluted and error-prone: We now need a separate synchronization object for each array element, and we must remember to specify the correct operation (getVolatile or setVolatile) upon each access. To make matters worse, access to the array's elements is not efficient because constant-folding optimizations cannot be applied.

      Concurrent map

      Deferred immutability can also be achieved with thread-safe maps such as ConcurrentHashMap, via the [<code class="prettyprint" >computeIfAbsent</code>] method:

      class OrderController {
      
          private final Map<Class<?>,Logger> logger = new ConcurrentHashMap<>();
      
          public Logger getLogger() {
              return logger.computeIfAbsent(OrderController.class, Logger::create);
          }
      
      }

      The JVM cannot trust the content of a map entry not to be updated after it is first added to the map, so constant-folding optimizations cannot be applied here.

      Risks and assumptions

      The JVM can apply constant-folding optimizations only when it can trust that final fields can be updated only once.

      Unfortunately, the core reflection API allows instance final fields to be updated arbitrarily, except for fields that are members of hidden classes or records. In the long term, we intend to limit the reflection API so that all instance final fields can be trusted, as part of the broader shift toward integrity by default. Until then, however, the mutability of most instance final fields will limit the constant-folding optimizations enabled by lazy constants.

      Fortunately, the reflection API does not allow static final fields to be updated arbitrarily, so constant folding across such fields is not only possible but routine. Thus, the examples shown above that store lazy constants, maps, or lists in static final fields will have good performance.

            pminborg Per-Ake Minborg
            pminborg Per-Ake Minborg
            Per-Ake Minborg Per-Ake Minborg
            Brian Goetz
            Votes:
            0 Vote for this issue
            Watchers:
            8 Start watching this issue

              Created:
              Updated: