RFR: 8351565: Implement JEP 502: Stable Values (Preview) [v33]

Viktor Klang vklang at openjdk.org
Wed Apr 2 18:59:17 UTC 2025


On Wed, 2 Apr 2025 13:22:44 GMT, Per Minborg <pminborg at openjdk.org> wrote:

>> Implement JEP 502.
>> 
>> The PR passes tier1-tier3 tests.
>
> Per Minborg has updated the pull request incrementally with one additional commit since the last revision:
> 
>   Add info that Map#values and Map#entrySet are stable

Thanks for all the great work here, @minborg!

I've accumulated some comments, suggestions, and questions—I hope they are helpful. Let me know if you want me to elaborate or explain anything.

(I haven't reviewed the benchmarks (yet))

src/java.base/share/classes/java/lang/StableValue.java line 271:

> 269:  * public final class SqrtUtil {
> 270:  *
> 271:  *     private SqrtUtil(){}

Suggestion:

 *     private SqrtUtil() {}

src/java.base/share/classes/java/lang/StableValue.java line 277:

> 275:  *     private static final Map<Integer, Double> SQRT =
> 276:  *             // @link substring="map" target="#map(Set,Function)" :
> 277:  *             StableValue.map(Set.of(1, 2, 4, 8, 16, 32), StrictMath::sqrt);

Why not used CACHED_KEYS here?

src/java.base/share/classes/java/lang/StableValue.java line 296:

> 294:  *
> 295:  * <h2 id="composition">Composing stable values</h2>
> 296:  * A stable value can depend on other stable values, thereby creating a dependency graph

Suggestion:

 * A stable value can depend on other stable values, forming a dependency graph

src/java.base/share/classes/java/lang/StableValue.java line 304:

> 302:  * public final class DependencyUtil {
> 303:  *
> 304:  *     private DependencyUtil(){}

Suggestion:

 *     private DependencyUtil() {}

src/java.base/share/classes/java/lang/StableValue.java line 353:

> 351:  * }
> 352:  *}
> 353:  * Both {@code FIB} and {@code Fibonacci::fib} recurses into each other. Because the

Suggestion:

 * Both {@code FIB} and {@code Fibonacci::fib} recurse into each other. Because the

src/java.base/share/classes/java/lang/StableValue.java line 374:

> 372:  * If there are circular dependencies in a dependency graph, a stable value will
> 373:  * eventually throw a {@linkplain StackOverflowError} upon referencing elements in
> 374:  * a circularity.

Is this still correct, I thought it detected reentrancy?

src/java.base/share/classes/java/lang/StableValue.java line 377:

> 375:  *
> 376:  * <h2 id="thread-safety">Thread Safety</h2>
> 377:  * The content of a stable value is guaranteed to be set at most once. If competing

Suggestion:

 * The contents of a stable value is guaranteed to be set at most once. If competing

src/java.base/share/classes/java/lang/StableValue.java line 384:

> 382:  * (e.g. {@linkplain #trySet(Object) trySet()})
> 383:  * {@linkplain java.util.concurrent##MemoryVisibility <em>happens-before</em>}
> 384:  * any subsequent read operation (e.g. {@linkplain #orElseThrow()}) that is successful.

Suggestion:

 * any successful read operation (e.g. {@linkplain #orElseThrow()}).

src/java.base/share/classes/java/lang/StableValue.java line 426:

> 424:  *           {@linkplain java.lang.ref##reachability strongly reachable}. Clients are
> 425:  *           advised that {@linkplain java.lang.ref##reachability reachable} stable values
> 426:  *           will hold their set content perpetually.

This reads like the contents of StableValues will never be garbage collected, even if the StableValue is no longer reachable. Is that intended?

src/java.base/share/classes/java/lang/StableValue.java line 567:

> 565:      * <p>
> 566:      * The provided {@code original} supplier is guaranteed to be successfully invoked
> 567:      * at most once even in a multi-threaded environment. Competing threads invoking the

There's a bit of a mix of "at most once" and "at-most-once". I'm fine with either but I prefer uniformity in spelling :)

src/java.base/share/classes/java/lang/StableValue.java line 589:

> 587: 
> 588:     /**
> 589:      * {@return a new stable int function}

"int function" reads strange to me. If we look at something like IntStream, it refers to "function": https://docs.oracle.com/en/java/javase/24/docs/api/java.base/java/util/stream/IntStream.html#mapToObj(java.util.function.IntFunction)

src/java.base/share/classes/jdk/internal/lang/stable/StableEnumFunction.java line 62:

> 60:     @Override
> 61:     public R apply(E value) {
> 62:         if (!member.test(value.ordinal())) {

Suggestion:

        if (!member.test(value.ordinal())) { // Implicit null-check of value

src/java.base/share/classes/jdk/internal/lang/stable/StableEnumFunction.java line 89:

> 87:     @Override
> 88:     public String toString() {
> 89:         final Collection<Map.Entry<E, StableValueImpl<R>>> entries = new ArrayList<>();

Might be good to size the entries to at most `size()`?

src/java.base/share/classes/jdk/internal/lang/stable/StableEnumFunction.java line 115:

> 113: 
> 114:         final int size = max - min + 1;
> 115:         final Class<E> enumType = (Class<E>)inputs.iterator().next().getClass();

I presume there's some external check to ensure that inputs is non-empty?

src/java.base/share/classes/jdk/internal/lang/stable/StableValueImpl.java line 93:

> 91:         if (!trySet(content)) {
> 92:             throw new IllegalStateException("Cannot set the content to " + content +
> 93:                     " because the content is already set: " + orElseThrow());

This might be problematic since it could surface confidential information inside exception messages (or even just produce very large strings).

src/java.base/share/classes/jdk/internal/lang/stable/StableValueImpl.java line 125:

> 123:         Objects.requireNonNull(supplier);
> 124:         final Object t = wrappedContentAcquire();
> 125:         return (t == null) ? orElseSetSlowPath(supplier) : unwrap(t);

Is it faster to have the slow path on the true-case?

src/java.base/share/classes/jdk/internal/lang/stable/StableValueImpl.java line 168:

> 166:     private void preventReentry() {
> 167:         if (Thread.holdsLock(this)) {
> 168:             throw new IllegalStateException("Recursing supplier detected");

Perhaps "Recursive initialization is not supported"

src/java.base/share/classes/jdk/internal/lang/stable/StableValueImpl.java line 176:

> 174:         assert Thread.holdsLock(this);
> 175:         // This upholds the invariant, a `@Stable` field is written to at most once
> 176:         // We know we hold the monitor here so plain semantic is enough

Perhaps better to move this to a javadoc/comment for the method itself: "must only be invoked while holding the monitor"

src/java.base/share/classes/jdk/internal/lang/stable/StableValueImpl.java line 191:

> 189:     @ForceInline
> 190:     private static Object wrap(Object t) {
> 191:         return (t == null) ? NULL_SENTINEL : t;

Personal preference, but it would seem more consistent to do the same check in wrap as in unwrap:

Suggestion:

        return (t != null) ? t : NULL_SENTINEL;

test/jdk/java/lang/StableValue/StableFunctionTest.java line 86:

> 84:         basic(inputs, MAPPER);
> 85:         basic(inputs, _ -> null);
> 86:     }

Test for null mapper?

test/jdk/java/lang/StableValue/StableFunctionTest.java line 101:

> 99:         assertTrue(cached.toString().endsWith("}"));
> 100:         // One between the values
> 101:         assertEquals(1L, cached.toString().chars().filter(ch -> ch == ',').count(), cached.toString());

Perhaps factor out the testing of the individual methods to different test cases to aid in debugging?

Perhaps only call `cached.toString()` once?

test/jdk/java/lang/StableValue/StableFunctionTest.java line 107:

> 105:         assertTrue(x1.getMessage().contains("ILLEGAL"));
> 106:         var x2 = assertThrows(IllegalArgumentException.class, () -> cached.apply(Value.ILLEGAL_AFTER));
> 107:         assertTrue(x2.getMessage().contains("ILLEGAL"));

Only tests a part of the message?

test/jdk/java/lang/StableValue/StableFunctionTest.java line 115:

> 113:         Function<Value, Integer> f0 = StableValue.function(inputs, Value::asInt);
> 114:         Function<Value, Integer> f1 = StableValue.function(inputs, Value::asInt);
> 115:         assertTrue(f0.toString().contains("{}"));

Not possible to test full string?

test/jdk/java/lang/StableValue/StableFunctionTest.java line 136:

> 134:         assertTrue(cached.toString().contains(Value.THIRTEEN + "=.unset"));
> 135:         assertTrue(cached.toString().contains(Value.FORTY_TWO + "=.unset"), cached.toString());
> 136:         assertTrue(cached.toString().endsWith("}"));

Not possible to test full toString() representation?

test/jdk/java/lang/StableValue/StableFunctionTest.java line 146:

> 144:         ref.set(cached);
> 145:         cached.apply(Value.FORTY_TWO);
> 146:         String toString = cached.toString();

I'd recommend caching the toString() representation in the other test cases as well 👍

test/jdk/java/lang/StableValue/StableFunctionTest.java line 147:

> 145:         cached.apply(Value.FORTY_TWO);
> 146:         String toString = cached.toString();
> 147:         assertTrue(toString.contains("(this StableFunction)"), toString);

Not possible to test full toString() representation?

test/jdk/java/lang/StableValue/StableListTest.java line 114:

> 112:         Integer[] arr = new Integer[SIZE];
> 113:         arr[INDEX] = 1;
> 114:         assertSame(arr, StableValue.list(INDEX, IDENTITY).toArray(arr));

No test for contents?

test/jdk/java/lang/StableValue/StableListTest.java line 283:

> 281:         var lazy = StableValue.list(SIZE, i -> ref.get().apply(i));
> 282:         ref.set(lazy::get);
> 283:         assertThrows(IllegalStateException.class, () -> lazy.get(INDEX));

Perhaps test the exception message to verify that it is indeed a recursion detection?

test/jdk/java/lang/StableValue/StableMapTest.java line 143:

> 141:             assertTrue(actual.contains(key + "=.unset"));
> 142:         }
> 143:         assertTrue(actual.endsWith("}"));

I guess the problem here is that the order of a map is not deterministic?

test/jdk/java/lang/StableValue/StableMapTest.java line 178:

> 176:         }
> 177:         assertTrue(entrySet.toString().startsWith("["));
> 178:         assertTrue(entrySet.toString().endsWith("]"));

Cache toString() repr?

test/jdk/java/lang/StableValue/StableMapTest.java line 201:

> 199:             } else {
> 200:                 assertTrue(map.toString().contains(key + "=.unset"));
> 201:             }

Cache map.toString()

test/jdk/java/lang/StableValue/StableValueFactoriesTest.java line 36:

> 34: import static org.junit.jupiter.api.Assertions.*;
> 35: 
> 36: final class StableValueFactoriesTest {

Just confirming—this is all that needs testing here?

test/jdk/java/lang/StableValue/StableValueTest.java line 262:

> 260:         threads.forEach(StableValueTest::join);
> 261:         // There can only be one winner
> 262:         assertEquals(1, winner.cardinality());

AFAIK BitSet is not thread safe, so you have multiple threads writing to it—which may or may not provide the right guarantees to lean on for this test. Might be better to lean on CHM.

test/jdk/java/lang/StableValue/StableValuesSafePublicationTest.java line 64:

> 62:         // fully initialized thanks to the HB properties of StableValue.
> 63:         int a;
> 64:         int b;

Might be worth putting in some more fields to increase the chance of detecting a mismatch.

test/jdk/java/lang/StableValue/StableValuesSafePublicationTest.java line 153:

> 151:         try {
> 152:             for (Thread t:threads) {
> 153:                 long deadline = System.currentTimeMillis()+TimeUnit.MINUTES.toMillis(1);

Why currentTimeMillis() here and nanoTime() above?

test/jdk/java/lang/StableValue/TrustedFieldTypeTest.java line 120:

> 118: //            assertThrows(IllegalAccessException.class, () -> {
> 119:             field.set(stableValue, 13);
> 120: //            });

Leftover comments?

-------------

PR Review: https://git.openjdk.org/jdk/pull/23972#pullrequestreview-2736446008
PR Review Comment: https://git.openjdk.org/jdk/pull/23972#discussion_r2024928142
PR Review Comment: https://git.openjdk.org/jdk/pull/23972#discussion_r2024929547
PR Review Comment: https://git.openjdk.org/jdk/pull/23972#discussion_r2024930451
PR Review Comment: https://git.openjdk.org/jdk/pull/23972#discussion_r2024930970
PR Review Comment: https://git.openjdk.org/jdk/pull/23972#discussion_r2024932995
PR Review Comment: https://git.openjdk.org/jdk/pull/23972#discussion_r2024942765
PR Review Comment: https://git.openjdk.org/jdk/pull/23972#discussion_r2024943338
PR Review Comment: https://git.openjdk.org/jdk/pull/23972#discussion_r2024946107
PR Review Comment: https://git.openjdk.org/jdk/pull/23972#discussion_r2024954825
PR Review Comment: https://git.openjdk.org/jdk/pull/23972#discussion_r2024978737
PR Review Comment: https://git.openjdk.org/jdk/pull/23972#discussion_r2025012681
PR Review Comment: https://git.openjdk.org/jdk/pull/23972#discussion_r2025045029
PR Review Comment: https://git.openjdk.org/jdk/pull/23972#discussion_r2025073406
PR Review Comment: https://git.openjdk.org/jdk/pull/23972#discussion_r2025077934
PR Review Comment: https://git.openjdk.org/jdk/pull/23972#discussion_r2025200236
PR Review Comment: https://git.openjdk.org/jdk/pull/23972#discussion_r2025201637
PR Review Comment: https://git.openjdk.org/jdk/pull/23972#discussion_r2025364611
PR Review Comment: https://git.openjdk.org/jdk/pull/23972#discussion_r2025365819
PR Review Comment: https://git.openjdk.org/jdk/pull/23972#discussion_r2025368554
PR Review Comment: https://git.openjdk.org/jdk/pull/23972#discussion_r2025377572
PR Review Comment: https://git.openjdk.org/jdk/pull/23972#discussion_r2025380044
PR Review Comment: https://git.openjdk.org/jdk/pull/23972#discussion_r2025380728
PR Review Comment: https://git.openjdk.org/jdk/pull/23972#discussion_r2025381378
PR Review Comment: https://git.openjdk.org/jdk/pull/23972#discussion_r2025383765
PR Review Comment: https://git.openjdk.org/jdk/pull/23972#discussion_r2025384849
PR Review Comment: https://git.openjdk.org/jdk/pull/23972#discussion_r2025384097
PR Review Comment: https://git.openjdk.org/jdk/pull/23972#discussion_r2025388876
PR Review Comment: https://git.openjdk.org/jdk/pull/23972#discussion_r2025392156
PR Review Comment: https://git.openjdk.org/jdk/pull/23972#discussion_r2025395553
PR Review Comment: https://git.openjdk.org/jdk/pull/23972#discussion_r2025396239
PR Review Comment: https://git.openjdk.org/jdk/pull/23972#discussion_r2025396838
PR Review Comment: https://git.openjdk.org/jdk/pull/23972#discussion_r2025400727
PR Review Comment: https://git.openjdk.org/jdk/pull/23972#discussion_r2025404972
PR Review Comment: https://git.openjdk.org/jdk/pull/23972#discussion_r2025407199
PR Review Comment: https://git.openjdk.org/jdk/pull/23972#discussion_r2025409083
PR Review Comment: https://git.openjdk.org/jdk/pull/23972#discussion_r2025411022


More information about the core-libs-dev mailing list