JEP 502: Stable Values (Preview)

AuthorPer Minborg & Maurizio Cimadamore
OwnerPer-Ake Minborg
TypeFeature
ScopeSE
StatusCandidate
Componentcore-libs / java.lang
Discussioncore dash libs dash dev at openjdk dot org
EffortS
DurationS
Reviewed byAlex Buckley, Brian Goetz
Created2023/07/24 15:11
Updated2025/02/04 11:54
Issue8312611

Summary

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

Goals

Non-goals

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 only in 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:

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 stable value is an object, of type StableValue, that holds a single data value, its content. A stable value must be initialized some time before its content is first retrieved, and it is immutable thereafter. A stable value is a way to achieve deferred immutability.

Here is the OrderController class, rewritten to use a stable value for its logger:

class OrderController {

    // OLD:
    // private Logger logger = null;

    // NEW:
    private final StableValue<Logger> logger = StableValue.of();

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

    void submitOrder(User user, List<Product> products) {
        getLogger().info("order started");
        ...
        getLogger().info("order submitted");
    }

}

The logger field holds a stable value, created with the static factory method StableValue.of(). Initially the stable value is unset, i.e., it holds no content.

The getLogger method calls logger.orElseSet(...) on the stable value to retrieve its content. If the stable value was already set then the orElseSet method returns its content. If the stable value is unset then the orElseSet method initializes it with the value returned by invoking the provided lambda expression, causing the stable value to become set; the method then returns that value. The orElseSet method thus guarantees that a stable value is initialized before it is used.

Even though the stable value, once set, is immutable, we are not forced to initialize its content in a constructor or, for a static stable value, in a class initializer. Rather, we can initialize it on demand. Furthermore, the orElseSet method guarantees that the provided lambda expression is evaluated only once, even when logger.orElseSet(...) 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 Stable Value API, you must enable preview features:

  • Compile your program with javac --release 25 --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 stable values

Stable values 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:

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

The flexibility of stable values enables us to re-imagine the initialization of entire applications. In particular, we can compose stable values from other stable values. Just as we used a stable value to store the logger in the OrderController component, we can also use a stable value 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 StableValue<OrderController>   ORDERS   = StableValue.of();
    static final StableValue<ProductRepository> PRODUCTS = StableValue.of();
    static final StableValue<UserService>       USERS    = StableValue.of();

    public static OrderController orders() {
        return ORDERS.orElseSet(OrderController::new);
    }

    public static ProductRepository products() {
        return PRODUCTS.orElseSet(ProductRepository::new);
    }

    public static UserService users() {
        return USERS.orElseSet(UserService::new);
    }

}

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 orElseSet method of the corresponding stable value. Each component, moreover, initializes its sub-components, such as its logger, on demand in the same way.

There is, furthermore, mechanical sympathy between stable values and the Java runtime. Under the hood, the content of a stable value is stored in a non-final field annotated with the JDK-internal @Stable annotation. This annotation, a common feature of low-level JDK code, asserts that the VM may trust the field, even though non-final, not to change after the VM considers its value for optimization after its initial and only update. This allows the JVM to treat the content of a stable value as a constant, provided that the field which refers to the stable value is final. Thus the JVM can perform constant-folding optimizations for code that accesses immutable data through multiple levels of stable values, e.g., Application.orders().getLogger().

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

Specifying initialization at the declaration site

Our examples have, thus far, initialized stable values at their point of use by, e.g., calling logger.orElseSet(...) in the getLogger method. This allows the content to be computed using information available to the getLogger method. Unfortunately, however, it means that all access to the logger stable value must go through that method.

In this case it would be more convenient if we could specify how to initialize the stable value when we declare it, without actually initializing it. We can do this by using a stable supplier:

class OrderController {

    // OLD:
    // private final StableValue<Logger> logger = StableValue.of();
    //
    // Logger getLogger() {
    //     return logger.orElseSet(() -> Logger.create(OrderController.class));
    // }

    // NEW:
    private final Supplier<Logger> logger
        = StableValue.supplier(() -> Logger.create(OrderController.class));

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

}

Here logger is no longer a stable value, but, rather, a stable supplier, i.e., a Supplier of the content of an underlying stable value created from an original Supplier which can compute the content on demand. When a stable supplier is first created, via StableValue.supplier(...), the content of the underlying stable value is not yet initialized.

To access the logger, clients call logger.get() rather than getLogger(). The first invocation of logger.get() invokes the original supplier, i.e., the lambda expression provided to StableValue.supplier(...). It uses the resulting value to initialize the content of the stable supplier's underlying stable value, and then returns the result to the client. Subsequent invocations of logger.get() return the content immediately.

Using a stable supplier, rather than a stable value, improves maintainability. The declaration and initialization of the logger field are now adjacent, resulting in more readable code. The OrderController class no longer has to document the invariant that every logger access must go through the getLogger method, which we can now remove.

The JVM can, of course, perform constant-folding optimizations on code that accesses the content of stable values through stable suppliers.

Aggregating stable values

Many applications work with 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 stable list:

class Application {

    // OLD:
    // static final OrderController ORDERS = new OrderController();

    // NEW:
    static final List<OrderController> ORDERS
        = StableValue.list(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 stable value, but, rather a stable list, i.e., a List in which each element is the content of an underlying stable value. When a stable list is created, via StableValue.list(...), the stable list has a fixed size, in this case POOL_SIZE. The contents of the stable values underlying its list elements are not yet initialized.

To access the content, clients call ORDERS.get(...), passing it an index, rather than ORDERS.orElseSet(...). The first invocation of ORDERS.get(...) with a particular index invokes the initialization function, in this case expressed as a lambda function that ignores the index and invokes the constructor new OrderController(). It uses the resulting OrderController object to initialize the content of the indexed element's underlying stable value, and then returns that object to the client. Subsequent invocations of ORDERS.get(...) with the same index return the element's content immediately.

The elements of a stable 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.

Stable lists retain many of the benefits of stable suppliers, since the function used to initialize the list's elements is provided when defining the list. The JVM can, as usual, perform constant-folding optimizations on code that accesses the content of stable values through stable lists.

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 computeIfAbsent 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 to not be updated after it is first added to the map, so constant-folding optimizations cannot be applied here. Moreover, the computeIfAbsent method is null-hostile: If the computing function returns null then no new entry is added to the map, which makes this solution impractical in some cases.

Risks and assumptions

The JVM can perform 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 stable values.

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 stable values, suppliers, or lists in static final fields will have good performance.