On tearing

Remi Forax forax at univ-mlv.fr
Wed Apr 27 15:38:46 UTC 2022


> From: "Brian Goetz" <brian.goetz at oracle.com>
> To: "valhalla-spec-experts" <valhalla-spec-experts at openjdk.java.net>
> Sent: Wednesday, April 27, 2022 3:59:31 PM
> Subject: On tearing

> 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 |
> 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.

Writing immutable objects in Java is hard, there is already a check list: 
- be sure that your class in not only unmodifiable but really immutable, storing a mutable class in a field is an issue 
- do you have declared all fields final, otherwise you have a publication issue 
- your constructors do not leak "this", right ! 

so adding a forth item 
- the class is not a primitive class 
does not seem to be a big leap too me. 

I agree with the idea that this is like the serialization, you have two ways to bypass the constructor. 
It's not as bad as the serialization, where usually you can generate instances with any values. 
By contrast, with a primitive class 
- the default value bypass the constructors 
- you can create a new value by merging components bypassing the constructors. 

So supposing a straightforward implementation, creating a NonNegativeInt is Ok but creating an Int256Range is not. 
(NonNegativeInt: to have a negative integers, you have to change the sign bit: the default value does not set the sign bit and merging value will not set the sign bit out of thin air) 
(Int256Range: you can smash the components of a bigger range to the components of a smaller range) 

So the rules are quite arcane and the question is should we try to avoid people shooting themselves in the foot. 

For me, we should make the model clear, the compiler should insert a non user overridable default constructor but not more because using a primitive class is already an arcane construct. 
There is no point to nanny people here given that only experts will want to play with it. 

This is very similar to the way Java let you use volatile and do a ++ on the field, that's usually a bad idea but volatile is an arcane construct, not something people use every day. 

But we (the EG) can also fail, and make a primitive class too easy to use, what scare me is people using primitive class just because it's not nullable. 

Rémi 


More information about the valhalla-spec-observers mailing list