JEP draft: Computed Constants
Authors | Per Minborg, Maurizio Cimadamore |
Type | Feature |
Scope | SE |
Status | Draft |
Component | core-libs / java.util.concurrent |
Effort | S |
Duration | S |
Created | 2023/07/24 15:11 |
Updated | 2023/09/06 18:56 |
Issue | 8312611 |
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 withjava --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 withjshell --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 java.lang
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.