A stable value is a holder of contents that can be set at most once.
A StableValue<T> is typically created using the factory method
StableValue.of(). When created this way,
the stable value is unset, which means it holds no contents.
Its contents, of type T, can be set by calling
trySet(), setOrThrow(),
or orElseSet(). Once set, the contents
can never change and can be retrieved by calling orElseThrow()
, orElse(), or orElseSet().
Consider the following example where a stable value field "logger" is a
shallowly immutable holder of contents of type Logger and that is initially
created as unset, which means it holds no contents. Later in the example, the
state of the "logger" field is checked and if it is still unset,
the contents is set:
public class Component {
// Creates a new unset stable value with no contents
private final StableValue<Logger> logger = StableValue.of();
private Logger getLogger() {
if (!logger.isSet()) {
logger.trySet(Logger.create(Component.class));
}
return logger.orElseThrow();
}
public void process() {
getLogger().info("Process started");
// ...
}
}
If getLogger() is called from several threads, several instances of
Logger might be created. However, the contents can only be set at most once
meaning the first writer wins.
In order to guarantee that, even under races, only one instance of Logger is
ever created, the orElseSet() method can be used
instead, where the contents are lazily computed, and atomically set, via a
supplier. In the example below, the supplier is provided in the
form of a lambda expression:
public class Component {
// Creates a new unset stable value with no contents
private final StableValue<Logger> logger = StableValue.of();
private Logger getLogger() {
return logger.orElseSet( () -> Logger.create(Component.class) );
}
public void process() {
getLogger().info("Process started");
// ...
}
}
The getLogger() method calls logger.orElseSet() on the stable value to
retrieve its contents. If the stable value is unset, then orElseSet()
evaluates the given supplier, and sets the contents to the result; the result is then
returned to the client. In other words, orElseSet() guarantees that a
stable value's contents is set before it returns.
Furthermore, orElseSet() guarantees that out of one or more suppliers provided,
only at most one is ever evaluated, and that one is only ever evaluated once,
even when logger.orElseSet() is invoked concurrently. This property is crucial
as evaluation of the supplier may have side effects, for example, the call above to
Logger.create() may result in storage resources being prepared.
Stable Functions
Stable values provide the foundation for higher-level functional abstractions. A
stable supplier is a supplier that computes a value and then caches it into
a backing stable value storage for subsequent use. A stable supplier is created via the
StableValue.supplier() factory, by
providing an underlying
Supplier which is invoked when the stable supplier
is first accessed:
public class Component {
private final Supplier<Logger> logger =
StableValue.supplier( () -> Logger.getLogger(Component.class) );
public void process() {
logger.get().info("Process started");
// ...
}
}
A stable supplier encapsulates access to its backing stable value storage. This means
that code inside
Component can obtain the logger object directly from the
stable supplier, without having to go through an accessor method like
getLogger().
A stable int function is a function that takes an int parameter and
uses it to compute a result that is then cached by the backing stable value storage
for that parameter value. A stable IntFunction is created via the
StableValue.intFunction()
factory. Upon creation, the input range (i.e. [0, size)) is specified together
with an underlying IntFunction which is invoked at most once per input
value. In effect, the stable int function will act like a cache for the underlying
IntFunction:
final class PowerOf2Util {
private PowerOf2Util() {}
private static final int SIZE = 6;
private static final IntFunction<Integer> UNDERLYING_POWER_OF_TWO =
v -> 1 << v;
private static final IntFunction<Integer> POWER_OF_TWO =
StableValue.intFunction(SIZE, UNDERLYING_POWER_OF_TWO);
public static int powerOfTwo(int a) {
return POWER_OF_TWO.apply(a);
}
}
int result = PowerOf2Util.powerOfTwo(4); // May eventually constant fold to 16 at runtime
The
PowerOf2Util.powerOfTwo() function is a
partial function that only
allows a subset
[0, 5] of the underlying function's
UNDERLYING_POWER_OF_TWO
input range.
A stable function is a function that takes a parameter (of type T) and
uses it to compute a result (of type R) that is then cached by the backing
stable value storage for that parameter value. A stable function is created via the
StableValue.function() factory.
Upon creation, the input Set is specified together with an underlying
Function which is invoked at most once per input value. In effect, the
stable function will act like a cache for the underlying Function:
class Log2Util {
private Log2Util() {}
private static final Set<Integer> KEYS =
Set.of(1, 2, 4, 8, 16, 32);
private static final UnaryOperator<Integer> UNDERLYING_LOG2 =
i -> 31 - Integer.numberOfLeadingZeros(i);
private static final Function<Integer, Integer> LOG2 =
StableValue.function(KEYS, UNDERLYING_LOG2);
public static int log2(int a) {
return LOG2.apply(a);
}
}
int result = Log2Util.log2(16); // May eventually constant fold to 4 at runtime
The
Log2Util.log2() function is a
partial function that only allows
a subset
{1, 2, 4, 8, 16, 32} of the underlying function's
UNDERLYING_LOG2 input range.
Stable Collections
Stable values can also be used as backing storage for
unmodifiable collections. A
stable list
is an unmodifiable list, backed by an array of stable values. The stable list elements
are computed when they are first accessed, using a provided
IntFunction:
final class PowerOf2Util {
private PowerOf2Util() {}
private static final int SIZE = 6;
private static final IntFunction<Integer> UNDERLYING_POWER_OF_TWO =
v -> 1 << v;
private static final List<Integer> POWER_OF_TWO =
StableValue.list(SIZE, UNDERLYING_POWER_OF_TWO);
public static int powerOfTwo(int a) {
return POWER_OF_TWO.get(a);
}
}
int result = PowerOf2Util.powerOfTwo(4); // May eventually constant fold to 16 at runtime
Similarly, a stable map is an unmodifiable map whose keys are known at
construction. The stable map values are computed when they are first accessed,
using a provided Function:
class Log2Util {
private Log2Util() {}
private static final Set<Integer> KEYS =
Set.of(1, 2, 4, 8, 16, 32);
private static final UnaryOperator<Integer> UNDERLYING_LOG2 =
i -> 31 - Integer.numberOfLeadingZeros(i);
private static final Map<Integer, INTEGER> LOG2 =
StableValue.map(CACHED_KEYS, UNDERLYING_LOG2);
public static int log2(int a) {
return LOG2.get(a);
}
}
int result = Log2Util.log2(16); // May eventually constant fold to 4 at runtime
Composing stable values
A stable value can depend on other stable values, forming a dependency graph
that can be lazily computed but where access to individual elements can still be
performant. In the following example, a single
Foo and a
Bar
instance (that is dependent on the
Foo instance) are lazily created, both of
which are held by stable values:
public final class DependencyUtil {
private DependencyUtil() {}
public static class Foo {
// ...
}
public static class Bar {
public Bar(Foo foo) {
// ...
}
}
private static final Supplier<Foo> FOO = StableValue.supplier(Foo::new);
private static final Supplier<Bar> BAR = StableValue.supplier(() -> new Bar(FOO.get()));
public static Foo foo() {
return FOO.get();
}
public static Bar bar() {
return BAR.get();
}
}
Calling
bar() will create the
Bar singleton if it is not already
created. Upon such a creation, the dependent
Foo will first be created if
the
Foo does not already exist.
Another example, which has a more complex dependency graph, is to compute the
Fibonacci sequence lazily:
public final class Fibonacci {
private Fibonacci() {}
private static final int MAX_SIZE_INT = 46;
private static final IntFunction<Integer> FIB =
StableValue.intFunction(MAX_SIZE_INT, Fibonacci::fib);
public static int fib(int n) {
return n < 2
? n
: FIB.apply(n - 1) + FIB.apply(n - 2);
}
}
Both
FIB and
Fibonacci::fib recurse into each other. Because the
stable int function
FIB caches intermediate results, the initial
computational complexity is reduced from exponential to linear compared to a
traditional non-caching recursive fibonacci method. Once computed, the VM is free to
constant-fold expressions like
Fibonacci.fib(5).
The fibonacci example above is a directed acyclic graph (i.e.,
it has no circular dependencies and is therefore a dependency tree):
___________fib(5)____________
/ \
____fib(4)____ ____fib(3)____
/ \ / \
fib(3) fib(2) fib(2) fib(1)
/ \ / \ / \
fib(2) fib(1) fib(1) fib(0) fib(1) fib(0)
If there are circular dependencies in a dependency graph, a stable value will
eventually throw an
IllegalStateException upon referencing elements in
a circularity.
Thread Safety
The contents of a stable value is guaranteed to be set at most once. If competing
threads are racing to set a stable value, only one update succeeds, while the other
updates are blocked until the stable value is set, whereafter the other updates
observes the stable value is set and leave the stable value unchanged.
The at-most-once write operation on a stable value that succeeds
(e.g. trySet())
happens-before
any successful read operation (e.g. orElseThrow()).
A successful write operation can be either:
A successful read operation can be either:
The method orElseSet(Supplier) guarantees that the provided
Supplier is invoked successfully at most once, even under race.
Invocations of orElseSet(Supplier) form a total order of zero or
more exceptional invocations followed by zero (if the contents were already set) or one
successful invocation. Since stable functions and stable collections are built on top
of the same principles as orElseSet() they
too are thread safe and guarantee at-most-once-per-input invocation.
Performance
As the contents of a stable value can never change after it has been set, a JVM
implementation may, for a set stable value, elide all future reads of that
stable value, and instead directly use any contents that it has previously observed.
This is true if the reference to the stable value is a constant (e.g. in cases where
the stable value itself is stored in a
static final field). Stable functions
and collections are built on top of StableValue. As such, they might also be eligible
for the same JVM optimizations as for StableValue.