On tearing
Brian Goetz
brian.goetz at oracle.com
Wed Apr 27 13:59:31 UTC 2022
Several people have asked why I am so paranoid about tearing. This mail is about tearing; there’ll be another about user model stacking and performance models. (Please, let’s try to resist the temptation to jump to “the answer”.)
Many people are tempted to say “let it tear.” The argument for “let it tear” is a natural-sounding one; after all, tearing only happens when someone else has made a mistake (data race). It is super-tempting to say “Well, they made a mistake, they get the consequences”.
While there are conditions under which this would be a reasonable argument, I don’t think those conditions quite hold here, because from both the inside and the outside, B3 classes “code like a class.” Authors will feel free to use constructors to enforce invariants, and if the use site just looks like “Point”, clients will not be wanting to keep track of “is this one of those classes with, or without, integrity?” Add to this, tearing is already weird, and while it is currently allowed for longs and doubles, 99.9999% of Java developers have never actually seen it or had to think about it very carefully, because implementations have had atomic loads and stores for decades.
As our poster child, let’s take integer range:
__B3 record IntRange(long low, int high) {
public IntRange {
if (low > high) throw;
}
}
Here, the author has introduced an invariant which is enforced by the constructor. Clients would be surprised to find an IntRange in the wild that disobeys the invariant. Ranges have a reasonable zero value. This a an obvious candidate for B3.
But, I can make this tear. Imagine a mutable field:
/* mutable */ IntRange r;
and two threads racing to write to r. One writes IntRange(5, 10); the other writes IntRange(2,4). If the writes are broken up into two writes, then a client could read IntRange(5, 4). Worse, unlike more traditional races which might be eventually consistent, this torn value will be observable forever.
Why does this seem worse than a routine long tearing (which no one ever sees and most users have never heard of)? Because by reading the code, it surely seems like the code is telling me that IntRange(5, 4) is impossible, and having one show up would be astonishing. Worse, a malicious user can create such a bad value (statistically) at will, and then inject that bad value into code that depends on the invariants holding.
Not all values are at risk of such astonishment, though. Consider a class like:
__B3 record LongHolder(long x) { }
Given that a LongHolder can contain any long value, users of LongHolder are not expecting that the range is carefully controlled. There are no invariants for which breaking them would be astonishing; LongHolder(0x1234567887654321) is just as valid a value as LongHolder(3).
There are two factors here: invariants and transparency. The above examples paint the ranges of invariants (from none at all, to invariants that constrain multiple fields). But there’s also transparency. The second example was unsurprising because the API allowed us to pass any long in, so we were not surprised to see big values coming out. But if the relationship between the representation and the construction API is more complicated, one could imagine thinking the constructor has deterred all the surprising values, and then still see a surprising value. That longs might tear is less surprising because any combination of bits is a valid long, and there’s no way to exclude certain values when “constructing” a long.
Separately, there are different considerations at the declaration and use site. A user can always avoid tearing by avoiding data races, such as marking the field volatile (that’s the usual cure for longs and doubles.) But what we’re missing is something at the declaration site, where the author can say “I have integrity concerns” and constrain layout/access accordingly. We experimented with something earlier (“extends NonTearable”) in this area.
Coming back to “why do we care so much”. PLT_Hulk summarized JCiP in one sentence:
https://twitter.com/PLT_Hulk/status/509302821091831809
If Java developers have learned one thing about concurrency, it is: “immutable objects are always thread-safe.” While we can equivocate about whether B3.val are objects or not, this distinction is more subtle than we can expect people to internalize. (If people internalized “Write immutable classes, they will always be thread-safe”, that would be pretty much the same thing.) We cannot deprive them of the most powerful and useful guideline for writing safe code.
(To make another analogy: serialization lets objects appear to not obey invariants established in the constructor. We generally don’t like this; we should not want to encourage more of this.)
There are options here, but none are a slam dunk:
- Force all B3 values to be atomic, which will have a performance cost;
- Deny the ability to enforce invariants on B3 classes (no NonNegativeInt, no IntRange);
- Try to educate people about tearing (good luck);
- Put out bigger warning signs (e.g., IntRange.tearable) that people can’t miss;
- More declaration-site control over atomicity, so classes with invariants can ensure their invariants are defended.
I think the last is probably the most sane.
More information about the valhalla-spec-observers
mailing list