Definition
- StableValue
– a container that is empty at creation and set exactly once, typically via orElseSet(Supplier)on first access. - Lazy final – combines the laziness of deferred initialization with the optimization guarantees of
finalfields. - Thread-safe – guaranteed at-most-once initialization even under concurrent access, without explicit locking.
- JVM-optimizable – the JVM can constant-fold and inline stable values like final fields after initialization.
- Replaces – the double-checked locking pattern (
volatile+synchronized+ two null checks).
Double-checked locking is like two security guards checking if the vault door is open: Guard 1 checks without locking (fast path), Guard 2 checks again inside a locked room (synchronized). Error-prone if the guards do not coordinate perfectly. StableValue is an automatic vault door that opens exactly once when the first person arrives. Everyone else walks through the already-open door. No guards needed.
Understanding the Concepts
What are Stable Values?
Stable Values (JEP 502, preview in Java 24) provide a lazily initialized, at-most-once container that the JVM can treat almost like a final field for optimization. A StableValue is created empty and set exactly once, typically via a Supplier on first read.
Why does double-checked locking need replacement?
Double-checked locking (DCL) is the traditional pattern for thread-safe lazy initialization. It is verbose, error-prone, and easy to get wrong.
package com.bank.service;
import java.math.BigDecimal;
import java.util.Map;
public class RateService {
// WRONG: broken double-checked locking (missing volatile)
private Map<String, BigDecimal> rates;
public Map<String, BigDecimal> getRates() {
if (rates == null) { // check 1
synchronized (this) {
if (rates == null) { // check 2
rates = loadRatesFromDb(); // expensive
}
}
}
return rates;
}
// Bug: without volatile, Thread B may see a partially constructed Map!
// CORRECT but verbose: proper DCL
private volatile Map<String, BigDecimal> ratesFixed;
public Map<String, BigDecimal> getRatesFixed() {
Map<String, BigDecimal> local = ratesFixed; // read volatile once
if (local == null) {
synchronized (this) {
local = ratesFixed;
if (local == null) {
ratesFixed = local = loadRatesFromDb();
}
}
}
return local;
}
// Correct, but 12 lines for lazy init!
}
How do Stable Values simplify this?
package com.bank.service;
import java.lang.StableValue;
import java.math.BigDecimal;
import java.util.Map;
public class RateService {
private final StableValue<Map<String, BigDecimal>> rates = StableValue.of();
public Map<String, BigDecimal> getRates() {
return rates.orElseSet(this::loadRatesFromDb);
// First call: runs loadRatesFromDb(), stores result
// All subsequent calls: returns cached result instantly
// Thread-safe: supplier runs at most once
}
}
Thread A calls getRates() → loadRatesFromDb() executes → result cached Thread B calls getRates() → returns cached result (no loading) Thread C calls getRates() concurrently with A → waits, then returns A's result
How is StableValue different from other lazy patterns?
| Pattern | Thread-safe | Correct | Lines | JVM-optimizable |
|---|---|---|---|---|
| Double-checked locking | If volatile | Error-prone | 12 | No |
| Lazy holder idiom | Yes | Yes | 8 | No |
| StableValue | Yes | By design | 2 | Yes (constant fold) |
| AtomicReference | Race possible | Supplier may run twice | 3 | No |
What are the rules for using StableValue?
- Create with
StableValue.of()and initialize withorElseSet(Supplier). - The Supplier runs at most once, even under concurrent access.
- After initialization, reads are as fast as reading a final field.
- Do not call
orElseSetwith different Suppliers on the same StableValue (the first one wins).
What are common mistakes?
// WRONG: trying to set the value twice
StableValue<String> name = StableValue.of();
name.orElseSet(() -> "Rajesh");
name.orElseSet(() -> "Priya"); // Returns "Rajesh" (first set wins)
// This is NOT an error, but the second supplier is never called
// WRONG: using StableValue for mutable state
StableValue<List<String>> list = StableValue.of();
list.orElseSet(ArrayList::new).add("item"); // Mutating the value defeats the purpose
// StableValue is for immutable lazy initialization
What should a senior engineer know?
- StableValue is preview in Java 24. The API may change before finalization.
- The JVM can constant-fold stable values: after initialization, the JIT may inline the value directly at call sites.
StableValue.ofList(size, indexMapper)creates a lazily-initialized list where each element is computed on first access.- For per-enum-constant caches,
StableValueis cleaner thanEnumMapwith lazy computation.
What are common interview follow-up questions?
- What happens if the Supplier throws? (The StableValue remains unset. The next call to
orElseSetwill try again.) - Is StableValue the same as AtomicReference.compareAndSet? (No. AtomicReference allows the Supplier to run multiple times under contention. StableValue guarantees at-most-once.)
- When is double-checked locking still appropriate? (On JDKs before 24, or when you cannot use preview features.)
Visual Overview
Double-Checked Locking vs StableValue:
DCL (12 lines, error-prone):
┌────────────────────────────┐
│ if (value == null) { │ ◀─ check 1 (not synchronized)
│ synchronized (this) { │
│ if (value == null) { │ ◀─ check 2 (synchronized)
│ value = compute(); │
│ } │
│ } │
│ } │
│ return value; │
└────────────────────────────┘
Requires volatile! Easy to get wrong.
StableValue (2 lines, correct by design):
┌────────────────────────────┐
│ StableValue<T> sv = of(); │
│ return sv.orElseSet(sup); │
└────────────────────────────┘
Thread-safe. At-most-once. JVM-optimizable.
Key Takeaway
StableValue (JEP 502, preview in Java 24) is a lazy-final container initialized at most once. It replaces the error-prone double-checked locking pattern with a one-liner: rates.orElseSet(this::loadRatesFromDb). The Supplier runs at most once, even under concurrent access from multiple threads. After initialization, reads are as fast as reading a final field because the JVM can constant-fold the value.
Double-checked locking requires 12 lines of code with volatile, synchronized, and two null checks. Missing volatile is a common bug that causes partially constructed objects to be visible to other threads. The lazy holder idiom is correct but requires a dedicated inner class. StableValue eliminates both patterns with a clean, correct-by-construction API.
In a banking rate service, the FX rate table is expensive to load from the database and rarely changes. StableValue defers the load to first access and caches the result permanently. All subsequent reads are instant with no synchronization overhead.
Remember one sentence: StableValue is a lazy-final container that replaces double-checked locking with a thread-safe, JVM-optimizable one-liner.
---