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.)
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". 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.
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.
The other consequence we might have from this sort of tearing is if one
of the other fields is an OOP. If the GC is unaware of the significance
of the null field (and we'd like for the GC to stay unaware of this),
then it is possible to have a null value where one of the oop fields
(from a previous write) is non-null, keeping that object reachable even
when it is logically not reachable. (As an interesting connection, the
boolean here is "special" in the same way as the synthetic boolean
channel is in pattern matching -- it dictates whether the _other_
channels are valid. Which makes nullable values a good implementation
strategy for pattern carriers.)
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;
- If we do a narrow write, and only write the null field, we risk
pinning other OOPs in memory