Skip to content

Conversation

minborg
Copy link
Contributor

@minborg minborg commented Oct 2, 2025

Implement JEP 526: Lazy Constants (Second Preview)

The lazy list/map implementations are broken out from ImmutableCollections to a separate class.

The old benchmarks are not moved/renamed to allow comparison with previous releases.

java.util.Optional is updated so that its field is annotated with @Stable. This is to allow Optional instances to be held in lazy constants and still provide constant folding.


Progress

  • Change must be properly reviewed (1 review required, with at least 1 Reviewer)
  • Change must not contain extraneous whitespace
  • Commit message must refer to an issue
  • Change requires CSR request JDK-8366179 to be approved

Issues

  • JDK-8366178: Implement JEP 526: Lazy Constants (Second Preview) (Enhancement - P4)
  • JDK-8366179: Implement JEP 526: Lazy Constants (Second Preview) (CSR)

Reviewing

Using git

Checkout this PR locally:
$ git fetch https://git.openjdk.org/jdk.git pull/27605/head:pull/27605
$ git checkout pull/27605

Update a local copy of the PR:
$ git checkout pull/27605
$ git pull https://git.openjdk.org/jdk.git pull/27605/head

Using Skara CLI tools

Checkout this PR locally:
$ git pr checkout 27605

View PR using the GUI difftool:
$ git pr show -t 27605

Using diff file

Download this PR as a diff file:
https://git.openjdk.org/jdk/pull/27605.diff

@bridgekeeper
Copy link

bridgekeeper bot commented Oct 2, 2025

👋 Welcome back pminborg! A progress list of the required criteria for merging this PR into master will be added to the body of your pull request. There are additional pull request commands available for use with this pull request.

@openjdk
Copy link

openjdk bot commented Oct 2, 2025

❗ This change is not yet ready to be integrated.
See the Progress checklist in the description for automated requirements.

@openjdk
Copy link

openjdk bot commented Oct 2, 2025

@minborg The following labels will be automatically applied to this pull request:

  • compiler
  • core-libs
  • i18n
  • nio
  • security

When this pull request is ready to be reviewed, an "RFR" email will be sent to the corresponding mailing lists. If you would like to change these labels, use the /label pull request command.

* lazy constant may block indefinitely; no timeouts or cancellations are provided.
*
* <h2 id="performance">Performance</h2>
* As a lazy constant can never change after it has been initialized. Therefore,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* As a lazy constant can never change after it has been initialized. Therefore,
* A lazy constant can never change after it has been initialized. Therefore,

super();
}

@Override public boolean isEmpty() { return size == 0;}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
@Override public boolean isEmpty() { return size == 0;}
@Override public boolean isEmpty() { return size == 0; }

}

private static final StableValue<Boolean> SHUTDOWN_WRITE_BEFORE_CLOSE = StableValue.of();
private static final LazyConstant<Boolean> SHUTDOWN_WRITE_BEFORE_CLOSE = LazyConstant.of(new Supplier<Boolean>() {
Copy link

@jeffque jeffque Oct 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if there is a reason for not using BooleanSupplier.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately, BooleanSupplier does not extend Supplier.

/**
* If non-null, the value; if null, indicates no value is present
*/
@Stable
Copy link

@kermandev kermandev Oct 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn’t this also add a null sentinel to allow the folding of the empty case? Or is that irrelevant because empty would be the terminator of a chain.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Usually a null sentinel is used when null indicates something different. I think maybe to allow folding the empty case, we should probably add a new annotation or a new element-value to indicate this status and properly handle it in C1/C2 (similar to the field trusting in ciField)

// Factory

public static <T> LazyConstantImpl<T> ofLazy(Supplier<? extends T> computingFunction) {
return new LazyConstantImpl<>(computingFunction);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We might return the function if it is already a LazyConstantImpl.

@ExE-Boss
Copy link

ExE-Boss commented Oct 4, 2025

I’m gonna miss Stable Values, as it has some use cases which aren’t served by Lazy Constants, and on which I depend on in some of my code, so I’m stuck with using regular non‑final fields.


Also, in the JEP 526 table under “Flexible initialization with lazy constants”:

Update count Update location Constant folding? Concurrent updates?
final field 1 Constructor or static initializer Yes No
LazyConstant [0, 1] Anywhere Yes, after initialization Yes, by winner
Non-final field [0, ∞) Anywhere No Yes

The “Update location” of LazyConstant shouldn’t be “Anywhere”, as that was only accurate for StableValue, but not for LazyConstant, which is updated by calling the passed Supplier.

Similarly, concurrent updates are prevented for LazyConstants by using synchronized (this).

@ExE-Boss
Copy link

ExE-Boss commented Oct 4, 2025

Getting access to the underlying StableValue API with Lazy Constants is way too hacky and convoluted (but doable):

StableVar.java
/*
 * Any copyright is dedicated to the Public Domain.
 * https://creativecommons.org/publicdomain/zero/1.0/
 */

import java.util.NoSuchElementException;
import java.util.function.Supplier;

import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;

import static java.lang.System.identityHashCode;
import static java.util.Objects.requireNonNull;

/// Horrible awful hack to get access to raw stable values in JDK 26+.
@NullMarked
public sealed interface StableVar<T> permits StableHacks.StableVarImpl {
	boolean trySet(final T contents) throws NullPointerException, IllegalStateException;
	T orNull();
	T orElse(final T other) throws NullPointerException;
	T orElseThrow() throws NoSuchElementException;
	boolean isSet();
	T orElseSet(final Supplier<? extends T> supplier) throws NullPointerException, IllegalStateException;
	void setOrThrow(final T contents) throws NullPointerException, IllegalStateException;

	static <T> StableVar<T> of() {
		return StableHacks.newInstance();
	}
}

/// Encapsulates the actual implementation of `StableValue` on `LazyConstant`
///
/// @author ExE Boss
@NullMarked
/*package*/ final @Namespace class StableHacks {
	private StableHacks() throws InstantiationException { throw new InstantiationException(StableHacks.class.getName()); }

	private static final String UNSET_SUFFIX = ".unset";
	private static final Object UNSET = new Object() {
		@Override
		public int hashCode() {
			return 0;
		}

		@Override
		public String toString() {
			return "unset";
		}
	};

	private static final ScopedValue<?> SCOPE = ScopedValue.newInstance();
	private static final Supplier<?> SCOPE_GETTER = SCOPE::get;

	/*package*/ static final <T> StableVarImpl<T> newInstance() {
		return new StableValue<>();
	}

	/*package*/ sealed interface StableVarImpl<T> extends StableVar<T> {
	}

	private record StableValue<T>(
		// Implemented as a record so that the JVM treats this as a trusted final field
		// even when `-XX:+TrustFinalNonStaticFields` is not set
		LazyConstant<T> contents
	) implements StableVarImpl<T> {
		@SuppressWarnings("unchecked")
		private StableValue() {
			this(LazyConstant.<T>of((Supplier) SCOPE_GETTER));
		}

		private StableValue {
			if (contents.isInitialized()) throw new InternalError();
		}

		@SuppressWarnings("unchecked")
		private final ScopedValue<T> scope() {
			return (ScopedValue<T>) SCOPE;
		}

		private final void preventReentry() throws IllegalStateException {
			if (Thread.holdsLock(this)) {
				throw new IllegalStateException("Recursive initialization of a stable value is illegal");
			}
		}

		@Override
		public boolean trySet(final T contents) throws NullPointerException, IllegalStateException {
			requireNonNull(contents);
			if (this.contents.isInitialized()) return false;

			preventReentry();
			synchronized (this) {
				return this.setImpl(contents);
			}
		}

		@Override
		@SuppressWarnings("unchecked")
		public final @Nullable T orNull() {
			return unwrapUnset(((LazyConstant) this.contents).orElse(UNSET));
		}

		@Override
		public T orElse(T other) throws NullPointerException {
			return this.contents.orElse(other);
		}

		@Override
		public T orElseThrow() throws NoSuchElementException {
			{ final T contents; if ((contents = this.orNull()) != null) {
				return contents;
			} }
			throw new NoSuchElementException();
		}

		@Override
		public boolean isSet() {
			return this.contents.isInitialized();
		}

		@Override
		public T orElseSet(final Supplier<? extends T> supplier) throws NullPointerException, IllegalStateException {
			requireNonNull(supplier);
			{ final T contents; if ((contents = this.orNull()) != null) {
				return contents;
			} }
			return orElseSetSlowPath(supplier);
		}

		@Override
		public void setOrThrow(final T contents) throws NullPointerException, IllegalStateException {
			if (!trySet(contents)) {
				throw new IllegalStateException();
			}
		}

		private final T orElseSetSlowPath(
			final Supplier<? extends T> supplier
		) throws NullPointerException, IllegalStateException {
			preventReentry();
			synchronized (this) {
				{ final T contents; if ((contents = this.orNull()) != null) {
					return contents;
				} }

				final T newValue;
				this.setImpl(newValue = requireNonNull(supplier.get()));
				return newValue;
			}
		}

		private final boolean setImpl(final T contents) {
			assert Thread.holdsLock(this);
			if (this.contents.isInitialized()) {
				return false;
			}

			ScopedValue.where(this.scope(), contents).run(this.contents::get);
			return true;
		}

		@Override
		public final boolean equals(final Object obj) {
			return this == obj;
		}

		@Override
		public final int hashCode() {
			return identityHashCode(this);
		}

		@Override
		public String toString() {
			final Object contents;
			return renderValue(
				"StableValue",
				(contents = this.orNull()) != this
					? contents
					: "(this StableValue)"
			);
		}
	}

	@SuppressWarnings("unchecked")
	private static final <T> T unwrapUnset(final Object obj) {
		return (obj == UNSET) ? null : (T) obj;
	}

	private static final String renderValue(
		final String type,
		final @Nullable Object value
	) throws NullPointerException {
		return (value == null)
			? type.concat(UNSET_SUFFIX)
			: (type + '[' + value + ']');
	}
}

@liach
Copy link
Member

liach commented Oct 4, 2025

Hi @ExE-Boss, this new JEP describes how this functionality will be provided in the future:

Lazy constants cover the common, high-level use cases for lazy initialization. In the future we might consider providing stable access semantics directly, at a lower level, for reference, array, and primitive fields. This would address, for example, use cases where the computing function associated with a lazy constant is not known at construction.

This would be necessary, as there are usage patterns (such as nominal descriptors in ClassFile API) that would benefit from multiple assignment and a stable promoted read.

@burningtnt
Copy link

It's essential to provide a low-level API as those in StableValue. Completely migrating to factory pattern may forcing users to build their own LazyConstant wrapper as ExeBoss has down above.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Development

Successfully merging this pull request may close these issues.

7 participants