User model stacking: current status
John Rose
john.r.rose at oracle.com
Sun Jul 3 01:40:54 UTC 2022
On 5 May 2022, at 12:21, Brian Goetz wrote:
>> There are lots of other things to discuss here, including a
>> discussion of what does non-atomic B2 really mean, and whether there
>> are additional risks that come from tearing _between the null and the
>> fields_.
>
> So, let's discuss non-atomic B2s. (First, note that atomicity is
> only relevant in the heap; on the stack, everything is
> thread-confined, so there will be no tearing.)
>
> If we have:
>
> non-atomic __b2 class DateTime {
> long date;
> long time;
> }
>
> then the layout of a B2 (or a B3.ref) is really (long, long, boolean),
> not just (long, long), because of the null channel. (We may be able
> to hide the null channel elsewhere, but that's an optimization.)
That goes straight to a desired optimization, but it leaves something
valuable in the dust.
The valuable thing is one of the “affordances of references”, which
is that a reference to an immutable value can be safely published. This
is a core feature of the JMM that applies to all value-based classes.
The behavior you are citing is inconsistent with a reference to an
object containing an immutable field (of type `DateTime.val`). It is
consistent with a reference to a mutable field or to an array of type
`DateTime.val[]`, but none of our current wrapper types work like that.
(Arrays do, which is a problem with arrays.)
I see how you got there: You want to apply full flattening to
`DateTime.ref`, simply adding a boolean. That’s a nice data structure
but it departs from how we expect boxing of values to work. There are
extra races between the components of `DateTime`, as well as a race
between null and non-null states. With today’s value-based classes, a
mutable `DateTime` reference will only show races between null and
non-null, and between earlier and later pairs of field values. With
this proposed feature, a mutable reference will act as if the wrapper
object being referenced were no longer immutable, and not safely
published. I think this is too much of a sharp edge, even for an opt-in
feature.
What I would prefer here is a principle that boxes (including
`DateTime.ref`) are always safely publishable. The possibility to race
on individual object states should be confined to the value companion
type.
That somewhat reduces the optimization for heap variables of reference
type of non-atomics. I think that’s a fine price to pay, in order to
avoid putting new exceptions into the JMM’s current assurances about
safe publication. The optimizations on the val-companion are
unaffected. This is good: Reasoning about strange race conditions can
concentrate around uses of the val-companion, and all uses of the
ref-companion would be race-safe.
Part of my discomfort here is that when we say that the fields of a
value-based class are final is that we are telling users their instances
can be safely published. I don’t want to claw that back, even for a
corner case like explicitly non-atomic value classes.
I do see that there could be a workaround, if a class `Foo` allowed
field-races even on its reference companion `Foo.ref`: Manually make a
value-based wrapper class `AtomicFoo` which is (implicitly declared as)
atomic and has a final `Foo` field as its sole payload. In that case I
think the JMM will assure me (am I right?) that a variable of type
`AtomicFoo` accesses a stable set of `Foo` fields, even if that
`AtomicFoo` variable is updated by data races, because its nested field
is not raced. And that should be true even if the JVM aggressively
flattens `AtomicFoo` into the `Foo` fields plus two null channels.
That’s all consistent, but I think it will cause bugs as people fumble
around with a mix of `Foo` and `AtomicFoo` values in containers like
`ArrayList` or `Object[]`.
>
> If two threads racily write (d1, t1) and (d2, t2) to a shared mutable
> DateTime, it is possible for an observer to observe (d1, t2) or (d2,
> t1). Saying non-atomic says "this is the cost of data races".
(So of course that’s OK for mutable copies of `DateTime.val`, but
that’s not how references behave now or should behave in the future.)
> But additionally, if we have a race between writing null and (d, t),
> there is another possible form of tearing.
>
> Let's write this out more explicitly. Suppose that T1 writes a
> non-null value (d, t, true), and T2 writes null as (0, 0, false). Then
> it would be possible to observe (0, 0, true), which means that we
> would be conceivably exposing the zero value to the user, even though
> a B2 class might want to hide its zero.
This is another reason to confine races to the value companion, because
we are making a plan to protect value companions specially, for cases
like this.
> So, suppose instead that we implemented writing a null as simply
> storing false to the synthetic boolean field. Then, in the event of
> a race between reader and writer, we could only see values for date
> and time that were previously put there by some thread. This
> satisfies the OOTA (out of thin air) safety requirements of the JMM.
I think the right approach here is starting with the semantics of
value-based classes (which include safe publication) and working out the
allowed implementation techniques.
The semantics of a flattened ref are (or should be) that it must behave
*as if* it were a non-flattened ref. (“Should be”: We are talking
optimization here, not a changeable variation in the user model adopted
randomly as the JIT comes and goes.) A ref, in fact, to a VBC.
A non-flattened ref is a thing which you first query as to null-ness,
and then if non-null you can load the VBC’s field or fields. (Without
races.)
So if there is a null channel sitting inside or next to some data
fields, the read-access code has to first check for null, and if not
null then to load a consistent view of the fields, in such a way that
racing writes of null or other values do not impair the consistency.
The write-access code can write null by asserting the null flag and (as
others have observed) it is an implementation puzzle whether to “clear
out” the other storage. (My take is that the JVM could do this during
GC at a safepoint, but it is hard to do so at other times.)
The write-access code can write non-null by (atomically) setting the
field values and then (if that did not already de-assert the null
channel) de-asserting the null channel. Again, the fields should be
written as a group consistently, so as not to interfere with racing
reads or writes. The null channel need not be written consistently.
All this would imply that the size of a flattened ref, perhaps including
its null channel, should be no larger than a naturally atomic unit of
memory, which is 64 or maybe 128 bits today.
Your argument above, which I think I buy, is that is also probably
possible to place the null channel outside of the naturally atomic unit
that contains the other fields; this would allow 9-byte and 17-byte
refs.
Such a racing null channel, with non-racing payload fields, can be
modeled in classic Java in the JMM like this:
```
class RacyNullable<V extends ValueBasedClass> {
private non-final boolean isNull = true;
private static final Object GARB = new Object(); //any value OK,
even null
private non-final Object v = GARB; //null and GARB never observed
public V get() { return isNull ? null : (V) v; }
public void set(V v) {
if (v == null) { isNull = true; if (EAGER_CLEANUP) cleanup(); }
else { this.v = v; /*race here!*/ isNull = false; }
}
private final boolean EAGER_CLEANUP = false;
private void cleanup() { if (isNull) /*race here?*/ v = GARB; }
}
}
```
I think really nice flattened refs can be built with “as if”
semantics the follow that pattern. They won’t flatten quite as well
as some of the “no holds barred” cases discussed by the EG, but they
would behave… “as if” …they follow the JMM without surprises.
The one race (outside of the cleanup method) is innocuous if the cleanup
method is used with restraint. How to do that is a puzzle.
> …
> So we have a choice for how we implement writing nulls, with a
> pick-your-poison consequence:
>
> - If we do a wide write, and write all the fields to zero, we risk
> exposing a zero value even when the zero is a bad value;
Yes, that’s like flipping `EAGER_CLEANUP` above.
(After if we go to the trouble of making `C.val` access-controlled,
let’s not make racy refs let the cat back out of the bag!)
> - If we do a narrow write, and only write the null field, we risk
> pinning other OOPs in memory
That’s the one I prefer. I think it’s actually a reasonable thing
to try for. Basically, the GC would have to special-case those fields
in a similar way that it special-cases weak-reference fields. For
WR’s the GC clears them under certain non-local conditions. In this
case the GC would clear them under a very local condition, the setting
of the null channel. GC folks growl about requests like this, but I
think this one is reasonable.
— John
P.S. Next up, a long-ish study on how to put access control on `C.val`!
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://mail.openjdk.org/pipermail/valhalla-spec-observers/attachments/20220702/9fb2a222/attachment-0001.htm>
More information about the valhalla-spec-observers
mailing list