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

Reply via email to