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

Computed Constants

    XMLWordPrintable

Details

    • JEP
    • Status: Draft
    • P4
    • Resolution: Unresolved
    • None
    • core-libs
    • None
    • Per Minborg, Maurizio Cimadamore
    • Feature
    • Open
    • SE
    • S
    • S

    Description

      Summary

      Introduce computed constants, which are immutable value holders that are initialized at most once. Computed constants offer the performance and safety benefits of final fields, while offering greater flexibility as to the timing of initialization. This is a preview API.

      Goals

      • Decouple the initialization of computed constants from the initialization of their containing class or object.
      • Provide an easy and intuitive API for computed constants and collections thereof.
      • Enable constant folding optimizations for computed constants.
      • Support dataflow dependencies between computed constants.
      • Reduce the amount of static initializer code and/or field initialization to be executed.
      • Allow disentanglement of the soup of <clinit> dependencies by applying the above.
      • Uphold integrity and consistency, even in a multi-threaded environment.

      Non-goals

      It is not a goal to

      • Provide additional language support for expressing constant computation. This might be the subject of a future JEP.
      • Prevent or deprecate existing idioms for expressing lazy initialization.

      Motivation

      Most Java developers have heard the advice "prefer immutability" (Effective Java, Item 17). Immutability confers many advantages: an immutable object can only be in one state, which is carefully controlled by its constructor; immutable objects can be freely shared with untrusted code; immutability enables all manner of runtime optimizations. Java's main tool for managing immutability is final fields (and more recently, record classes). Unfortunately, final fields come with restrictions. They must be set early; final instance fields must be set by the end of the constructor, and static final fields during class initialization. Moreover, the order in which final field initializers are executed is determined at compile-time and then made explicit in the resulting class file. As such, the initialization of a final field is fixed in time; it cannot be arbitrarily moved forward or backward. This means that developers are forced to choose between finality and all its benefits, and flexibility over the timing of initialization. Developers have devised a number of strategies to ameliorate this imbalance, but none are ideal.

      For instance, monolithic class initializers can be broken up by leveraging the laziness already built into class loading. Often referred to as the class-holder idiom, this technique moves lazily initialized state into a helper class which is then loaded on-demand, so its initialization is only performed when the data is actually needed, rather than unconditionally initializing constants when a class is first referenced:

      // ordinary static initialization
      private static final Logger LOGGER = Logger.getLogger("com.foo.Bar");
      ...
      LOGGER.log(...);

      we can defer initialization until we actually need it:

      // Initialization-on-demand holder idiom
      Logger logger() {
          class Holder {
               static final Logger LOGGER = Logger.getLogger("com.foo.Bar");
          }
          return Holder.LOGGER;
      }
      ...
      logger().log(...);

      The code above ensures that the Logger object is created only when actually required. The (possibly expensive) initializer for the logger lives in the nested Holder class, which will only be initialized when the logger method accesses the LOGGER field. While this idiom works well, its reliance on the class loading process comes with significant drawbacks. First, each constant whose computation needs to be shifted in time generally requires its own holder class, thus introducing a significant static footprint cost. Second, this idiom is only really applicable if the field initialization is suitably isolated, not relying on any other parts of the object state. Alternatively, the double-checked locking idiom, can also be used for deferring evaluation of field initializers. The idea is to optimistically check if the field's value is non-null and if so, use that value directly; but if the value observed is null, then the field must be initialized, which, to be safe under multi-threaded access, requires acquiring a lock to ensure correctness:

      // Double-checked locking idiom
      class Foo {
          private volatile Logger logger;
          public Logger logger() {
              Logger v = logger;
              if (v == null) {
                  synchronized (this) {
                      v = logger;
                      if (v == null) {
                          logger = v = Logger.getLogger("com.foo.Bar");
                      }
                  }
              }
              return v;
          }
      }

      While the double-checked locking idiom can be used for both class and instance variables, its usage requires that the field subject to initialization is marked as non-final. This is not ideal for two reasons: first, it would be possible for code to accidentally modify the field value, thus violating the immutability assumption of the enclosing class. Second, access to the field cannot be adequately optimized by just-in-time compilers, as they cannot reliably assume that the field value will, in fact, never change. An example of similar optimizations in existing Java implementations is when a MethodHandle is held in a static final field, allowing the runtime to generate machine code that is competitive with direct invocation of the corresponding method.

      Further, the double-checked locking idiom is brittle and easy to get subtly wrong (see Java Concurrency in Practice, 16.2.4.)

      What we are missing -- in both cases -- is a way to promise that a constant will be initialized by the time it is used, with a value that is computed at most once. Such a mechanism would give the Java runtime maximum opportunity to stage and optimize its computation, thus avoiding the penalties (static footprint, loss of runtime optimizations) which plague the workarounds shown above.

      Description

      Preview Feature

      Computed Constants is a preview API, disabled by default. To use the Computed Constants API the JVM flag --enable-preview must be passed in, as follows:

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

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

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

      Outline

      The Computed Constants API defines classes and an interface so that client code in libraries and applications can

      • Define and use computed constant objects: ComputedConstant

      • Define and use computed constant collections: List<ComputedConstant>

      The Computed Constants API resides in the [<code class="prettyprint" >java.lang</code>] package of the java.base module.

      Computed Constant

      A computed constant is a holder object that is initialized at most once. It is guaranteed to be initialized no later than the time it is first accessed. It is expressed as an object of type ComputedConstant, which, like Future, is a holder for some computation that may or may not have occurred yet. ComputedConstant instances are created by providing a value supplier, typically in the form of a lambda expression or a method reference, which is computes the constant value:

      class Bar {
          // 1. Declare a computed constant value
          private static final ComputedConstant<Logger> LOGGER =
                  ComputedConstant.of( () -> Logger.getLogger("com.foo.Bar") );
      
          static Logger logger() {
              // 2. Access the computed value 
              //    (evaluation made before the first access)
              return LOGGER.get();
          }
      }

      Calling logger() multiple times yields the same value from each invocation. This is similar in spirit to the holder class idiom, and offers the same performance, constant-folding, and thread-safety characteristics, but is simpler and incurs a lower static footprint since no additional class is required. ComputedConstant guarantees the value supplier is invoked at most once per ComputedContant instance. In the LOGGER example above, the supplier is invoked at most once per loading of the containing class Bar (Bar, in turn, can be loaded at most once into any given ClassLoader). A value supplier may return null which will be considered the bound value. (Null-averse applications can also use ComputedConstant<Optional<V>>.)

      Computed Constants Collections

      While initializing a single field of type ComputedConstant is cheap (remember, creating a new ComputedConstant object only creates the holder for the lazily evaluated value), this (small) initialization cost has to be paid for each field of type ComputedConstant declared by the class. As a result, the class static and/or instance initializer will keep growing with the number of ComputedConstant fields, thus degrading performance.

      To handle these cases, the Computed Constants API provides a construct that allows the creation of a List of ComputedConstants elements. Such a List is a list whose elements are evaluated independently before a particular element is first accessed. Lists of computed constants are objects of type List<ComputedConstant>. Consequently, each element in the list enjoys the same properties as a ComputedConstant but with lower storage requirements.

      Like a ComputedConstant object, a List<ComputedConstant> object is created by providing an element provider - typically in the form of a lambda expression or method reference, which is used to compute the value associated with the i-th element of the List instance when the element is first accessed:

      class Labels {
      
          private static final ComputedConstant<ResourceBundle> BUNDLE = 
              ComputedConstant.of(
                  () -> ResourceBundle.getBundle("LabelsBundle", Locale.GERMAN)
              );
      
          private final List<ComputedConstant<String>> labels;
      
          public Labels(int size) {
              labels = ComputedConstant.of(
                      size,
                      i -> BUNDLE.get().getString(Integer.toString(i))
              );
          }
      
          /*
          # This is the LabelsBundle_de.properties file
          0 = Computer
          1 = Platte
          2 = Monitor
          3 = Tastatur
           */
      
          public static void main(String[] args) {
              var lbl = new Labels(4);
              var kbd = lbl.labels.get(3); // Tastatur
          }
      
      }
      

      Note how there's only one field of type List<ComputedConstant<String>> to initialize - every other computation is performed before the corresponding element of the list is accessed. Note also how the value of an element in the labels list, stored in an instance field, depends on the value of another ComputedConstant value (BUNDLE), stored in a static field. Finally, note that it also shows the usage of both static and instance variables. The Computed Constants API allows modeling this cleanly, while still preserving good constant-folding guarantees and integrity of updates in the case of multi-threaded access.

      Safety

      Initializing a computed constant is an atomic operation: calling ComputedConstant::get either results in successfully initializing the computed constant to a value, or fails with an exception. This is true regardless of whether the computed constant is accessed by a single thread, or concurrently, by multiple threads. Moreover, while computed constants can depend on each other, the API dynamically detects circular initialization dependencies and throws a StackOverflowError when a circularity is found:

      static ComputedConstant<Integer> a;
      static ComputedConstant<Integer> b;
      
         ...
      
          a = ComputedConstant.of( () -> b.get() );
          b = ComputedConstant.of( () -> a.get() );
      
          a.get(); 
      
      java.lang.StackOverflowError: Circular supplier detected
      ...

      Alternatives

      There are other classes in the JDK that support lazy computation including Map, AtomicReference, ClassValue, and ThreadLocal which are similar in the sense that they support arbitrary mutation and thus, prevent the JVM from reasoning about constantness and do not allow shifting computation before being used.

      So, alternatives would be to keep using explicit double-checked locking, maps, holder classes, Atomic classes, and third-party frameworks.

      Risks and Assumptions

      Creating an API to provide thread-safe computed constant fields with an on-par performance with holder classes efficiently is a non-trivial task. It is, however, assumed that the current JIT implementations will likely suffice to reach the goals of this JEP.

      Dependencies

      The work described here will likely enable subsequent work to provide pre-evaluated computed constant fields at compile, condensation, and/or runtime.

      Attachments

        Issue Links

          Activity

            People

              pminborg Per-Ake Minborg
              pminborg Per-Ake Minborg
              Votes:
              1 Vote for this issue
              Watchers:
              6 Start watching this issue

              Dates

                Created:
                Updated: