Back to Java 24/25 Preview
Q1Question 1 of 8

What are Stable Values?

Key Concepts
  • StableValue is a lazy-final container initialized at most once on first access, replacing double-checked locking
  • Thread-safe by design without explicit volatile, synchronized, or null checks
  • The JVM can optimize stable values like final fields with constant folding and inlining
  • Usage: StableValue.of() creates it, orElseSet(Supplier) initializes on first call
  • Eliminates the verbose and error-prone double-checked locking pattern
  • Preview in Java 24 (JEP 502), designed for expensive lazy initialization
  • Supplier runs at most once even under concurrent access from multiple threads

Definition

StableValue is a lazy-final container that initializes at most once on first access, replacing error-prone double-checked locking with a thread-safe one-liner.
  • 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 final fields.
  • 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.

JAVA
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?

JAVA
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
    }
}
DIAGRAM
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 with orElseSet(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 orElseSet with different Suppliers on the same StableValue (the first one wins).

What are common mistakes?

JAVA
// 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, StableValue is cleaner than EnumMap with lazy computation.

What are common interview follow-up questions?

  • What happens if the Supplier throws? (The StableValue remains unset. The next call to orElseSet will 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

DIAGRAM
  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.

---

Share
Use ← → arrow keys to navigate