JDK-8352433 : Behavioral updates for ClassValue::remove
  • Type: CSR
  • Component: core-libs
  • Sub-Component: java.lang
  • Priority: P4
  • Status: Closed
  • Resolution: Approved
  • Fix Versions: 25
  • Submitted: 2025-03-19
  • Updated: 2025-06-09
  • Resolved: 2025-05-15
Related Reports
CSR :  
Relates :  
Description
Summary
-------

Specification updates to `ClassValue` to clarify, simplify, and correct its behaviors. These changes are spurred by a revisit to the removal operation, which also simplifies other operations.

Problem
-------

The behaviors of `ClassValue` are not mentioned in the class-level specification, and are currently hard to obtain from the method descriptions.

In particular, the specification of `remove` is unnecessarily complex, and it has a few behavioral problems: it either cannot prevent association of a stale value (before JDK-8351045) or stalls in an infinite loop if it is called from `computeValue` (after JDK-8351045).

We want to make the mental model of `remove` and its interactions with `get` or `computeValue` simpler.

Solution
--------

Rewrite the specification of `ClassValue` so that all accesses on an association in a `ClassValue` (identified by a `ClassValue`-`Class` pair) have a total order, then modify the implementation to ensure that it conforms.

There are 3 kinds of accesses:

1. Read access: Reads the current associated value
2. Associate access: Try to associate the value from a `computeValue`
3. Remove access: Removes the current associated value

A `get` call first performs a read access; if the associated value is absent, it enters a loop of calling `computeValue` and trying to associate that computed value; the loop terminates when an associated value is present or `computeValue` failed with an exception, and retries if the most recent remove access does not happen-before the finish of the `computeValue`.

> This happens-before relationship means that a retry can always reestablish this relationship if there is no new remove access when the next `computeValue` call returns, and a previous remove on the same thread does not require a retry. So a retry happens if the latest remove access happens on a different thread after the read/install access that initiated the `computeValue`. This leniency toward reentrancy of `get` or `remove` is necessary for scenarios like class initialization. True infinite loops of `get` results in `StackOverflowError`, and duplicate `remove`s does not invalidate the value under computation on the same thread.

A `remove` is simple: A remove access simply clears an association and prevents future associating of values from computeValue whose completion that cannot observe this remove.

Specification
-------------

The old specifications on these methods were almost completely replaced; hence not provided in this section.

New class-level specification:
```
Lazily associate a computed value with any {@code Class} object.
For example, if a dynamic language needs to construct a message dispatch
table for each class encountered at a message send call site,
it can use a {@code ClassValue} to cache information needed to
perform the message send quickly, for each class encountered.
<p>
The basic operation of a {@code ClassValue} is {@link #get get}, which
returns the associated value, initially created by an invocation to {@link
#computeValue computeValue}; multiple invocations may happen under race, but
exactly one value is associated to a {@code Class} and returned.
<p>
Another operation is {@link #remove remove}: it clears the associated value
(if it exists), and ensures the next associated value is computed with input
states up-to-date with the removal.
<p>
For a particular association, there is a total order for accesses to the
associated value.  Accesses are atomic; they include:
<ul>
<li>A read-only access by {@code get}</li>
<li>An attempt to associate the return value of a {@code computeValue} by
{@code get}</li>
<li>Clearing of an association by {@code remove}</li>
</ul>
A {@code get} call always include at least one access; a {@code remove} call
always has exactly one access; a {@code computeValue} call always happens
between two accesses.  This establishes the order of {@code computeValue}
calls with respect to {@code remove} calls and determines whether the
results of a {@code computeValue} can be successfully associated by a {@code
get}.

@param <T> the type of the associated value
@author John Rose, JSR 292 EG
@since 1.7
```

New specification for `get`:
```
{@return the value associated to the given {@code Class}}
<p>
This method first performs a read-only access, and returns the associated
value if it exists.  Otherwise, this method tries to associate a value
from a {@link #computeValue computeValue} invocation until the associated
value exists, which could be associated by a competing thread.
<p>
This method may throw an exception from a {@code computeValue} invocation.
In this case, no association happens.

@param type the {@code Class} to retrieve the associated value for
@throws NullPointerException if the argument is {@code null}
@see #remove
@see #computeValue
```

New specification for `computeValue`:
```
Computes the value to associate to the given {@code Class}.
<p>
This method is invoked when the initial read-only access by {@link #get
get} finds no associated value.
<p>
If this method throws an exception, the initiating {@code get} call will
not attempt to associate a value, and may terminate by returning the
associated value if it exists, or by propagating that exception otherwise.
<p>
Otherwise, the value is computed and returned.  An attempt to associate
the return value happens, with one of the following outcomes:
<ul>
<li>The associated value is present; it is returned and no association
is done.</li>
<li>The most recent {@link #remove remove} call, if it exists, does not
happen-before (JLS {@jls 17.4.5}) the finish of the {@code computeValue}
that computed the value to associate.  A new invocation to {@code
computeValue}, which that {@code remove} call happens-before, will
re-establish this happens-before relationship.</li>
<li>Otherwise, this value is successfully associated and returned.</li>
</ul>

@apiNote
A {@code computeValue} call may, due to class loading or other
circumstances, recursively call {@code get} or {@code remove} for the
same {@code type}.  The recursive {@code get}, if the recursion stops,
successfully finishes and this initiating {@code get} observes the
associated value from recursion.  The recursive {@code remove} is no-op,
since being on the same thread, the {@code remove} already happens-before
the finish of this {@code computeValue}; the result from this {@code
computeValue} still may be associated.

@param type the {@code Class} to associate a value to
@return the newly computed value to associate
@see #get
@see #remove
```

New specification for `remove`:
```
Removes the associated value for the given {@code Class} and invalidates
all out-of-date computations.  If this association is subsequently
{@linkplain #get accessed}, this removal happens-before (JLS {@jls
17.4.5}) the finish of the {@link #computeValue computeValue} call that
returned the associated value.

@param type the type whose class value must be removed
@throws NullPointerException if the argument is {@code null}
```

Comments
Moving to Approved.
15-05-2025

Viktor asked me to look at the proposed changes to the API docs. Much simpler, and remove API docs are much easier to read. So I think this is good.
15-05-2025

Moving updated request back to Provisional.
01-05-2025

I discussed with John and we figured out a much more simplified model - this also does not require excessive disclosure into the implementation techniques.
30-04-2025

Moving to Provisional. Hmm. I would find it helpful if there were a short paragraph along the lines of: "The methods on this class do what you want, even in the presence of multi-threading of get and remove. In more detail that means..." and this more detailed discussion could be handled by implSpec tags.
29-04-2025