[concurrency-interest] Should I avoid compareAndSet with value-based classes?

Gil Tene gil at azul.com
Sat Jul 8 09:17:24 EDT 2017


With all this said (and I absolutely think that e.g. it should be expected that loops like my examples from the previous post may never terminate), this discussion has made me come up with a way to (I *think*) safely apply CAS to instances of Instant (and on any other instances that happen to be of value-based classes) as long as we code the CAS and the type it operates on carefully. I believe it is safe to do so as long as the field being CAS'ed into is declared as an Object and NOT as an Instant. By casting from Object to Instant to perform Instant operations [e.g. adding a duration] while preserving the Object form for the thing being compared in the CAS operation, I believe the CAS remains valid. See code below.

The reason that I believe this approach is inherently safe is that the contents of fields and local variables of type Object are [I think] required to retain identity-sensitive operation behaviors regardless of the actual (derived from Object) type of the current contents. The contents of fields and variables *known* to be of type Instant (and of other value-based classes that are inherited from Object) may lose identity information and produce unpredictable behaviors when used with identity-sensitive operations, based on the specification of the behaviors of value-based classes. However, this freedom to lose track of identity does not extend to fields and variables of type Object (or other non-value-based classes).

In effect, I believe that the JVM is required to "box" instances of value-based classes in actual object references to actual objects with actual identity when storing them in fields or variables of a type that is not a value-based class. And all identity-based operations done on those fields and variables remain as-expected regardless of whether or not the contents is a reference to an instance of a value-based class. This quality is lost once something is cast to a value-based class (for storage in either a variable or field). In back-and-forth casting (implicit or explicit), the identities of objects that are created by casting back to Object does not have to remain the same as the identities of the original Objects cast from to a value-based class.

[** in this logic, "field" includes an array element]

This also leads me to [partially] retract my suggestion for throwing exceptions when encountering identity-based operations on instances of value-based classes. Such exceptions clearly shouldn't be thrown when the declared types of the operands involved are not value-based classes (even if the contents is of those type). I still think that throwing exceptions when the declared types are value-based classes could make sense, but this can actually be done better by the Java compiler refusing to compile such code (and it should know enough to do that, since the types are declared).

Since Generics do reification, this stuff would be un-enforceable in the generics source code. But it can still be enforced in code that uses generics, e.g. a AtomicReference<Object>.compareAndSet() can still refuse [at compile time] to accept a declared-as-value-based-class type as it's "expected" parameter.

public class MutableClock {

    public static MutableClock create(final Instant instant, final ZoneId zone) {
        return new MutableClock(
                new AtomicReference<>(instant),
                zone);
    }

    // Instants are held in an Object field to allow identity-based CAS operations:
    private final AtomicReference<Object> instantHolder;
    private final ZoneId zone;

    private MutableClock(
            final AtomicReference<Object> instantHolder,
            final ZoneId zone) {
        this.instantHolder = instantHolder;
        this.zone = zone;
    }

    public Instant instant() {
        return (Instant) instantHolder.get();
    }

    public ZoneId getZone() {
        return zone;
    }

    public void setInstant(final Instant newInstant) {
        instantHolder.set(newInstant);
    }

    void add(Duration amountToAdd) {
        boolean success = false;
        do {
            Object currentContents = instantHolder.get();
            // ((Instant) currentContents) may have no identity, but currentContents does...
            Instant newInstant = ((Instant) currentContents).plus(amountToAdd);
            // Compare part of CAS would not be valid for an Instant field,
            // but is valid for an Object field:
            success = instantHolder.compareAndSet(currentContents, newInstant);
        } while (!success);

        // the above is equivalent to this, I believe:
        //   instantHolder.updateAndGet(instant -> ((Instant)instant).plus(amountToAdd));
    }

    public MutableClock withZone(final ZoneId newZone) {
        // conveniently, AtomicReference also acts as a
        // vehicle for "shared updates" between instances:
        return new MutableClock(instantHolder, newZone);
    }
}




On Jul 8, 2017, at 5:22 AM, Gil Tene <gil at azul.com<mailto:gil at azul.com>> wrote:


On Jul 8, 2017, at 1:19 AM, Andrew Haley <aph at redhat.com<mailto:aph at redhat.com>> wrote:

On 08/07/17 07:38, Gil Tene wrote:

My arguments here are not about trying to justify some compiler
optimizations, or trying to justify the choices of making these
things subclasses the Object. They are about pointing out the actual
meaning of things as defined and the (very high risk) of coding
against it based on a hope that some temporarily observed behavior
is actually reliable, when everything that describes what it does
says otherwise.

I disagree with this interpretation.  You're interpreting the language
in that paragraph as overriding some fundamental properties in the JLS.

To keep this to a simple case that

What specific property of the JLS requires that == not be always-false?

I.e., what is it in the JLS that requires that this loop ever terminate when there is no external interference by another thread?:

 // David Lloyd's example from earlier in the thread (Jul 6, 10:48AM PDT):
 AtomicReference<Instant> atomic;
 Instant a, b;
 ...
 do {
     a = atomic.get();
     b = compute(a);
 } while (! atomic.compareAndSet(a, b));


or for that matter, that this loop ever terminate when there is no external interference by another thread?:

 volatile Instant thing;
 Instant a, b;
 …
 boolean success = false;
 do {
     a = thing;
     b = compute(a);
     if (thing == a) {
         thing = b;
         success = true;
     }
 } while (!success);

The termination of both of these loops relies on the notion of identity (the logical value of a reference) being preserved between the reading of a reference to an instance of a value-based class from memory, and the eventual comparison of that instance's reference with another one stored in a memory location.

I'm really asking… I'd love to find something in the JLS that says this must be preserved, and thereby contradict the statements that say that instances of value-based classes "are freely substitutable when equal" and "Use of such identity-sensitive operations on instances of value-based classes may have unpredictable effects".

I'd like to point out that Instant happens to be one of those cool ""partially compressible value" classes who's value cannot be fully represented with a single 64 bit value, but where "a wide and interesting range" of values can be: specifically, all the possible values of Instant between the epoch and some time in the year 2043 *can* be represented in 63 bits, making smalltalk-like inlined "it's not a pointer, it's a value" representations very possible (e.g. negative references are actual the values) and appealing for many reasons (both space and speed). This works as long as both identity and type can be erased from the contents without hurting computations [which can be done safely when stored in registers whose type is known, for example]. And this quality is specifically provided by the documentation of the type as a value-based class.

However, since it is possible for instances of Instant to be stored in locations that do not have this quality [e.g. Object fields, or even known-to-be-of-type-Instant fields in memory depending on implementation], each possible "compressible value" also needs a non-compressed representation in a heap-based object contents form. This means that the value in the memory location may be a true reference, while the value we read into local variables may not be, at least for a wide range of possible values. And that in turn means that the loops above will never terminate (since they will never be able to show that the reference contents of "atomic" or of "thing" is == to the value in a).


In the case of C, the thing to do would be to ask for a clarification,
but I suspect that if we asked, say, John Rose, Alex Buckley, and
Brian Goetz we'd get different answers.  ;-)

--
Andrew Haley
Java Platform Lead Engineer
Red Hat UK Ltd. <https://www.redhat.com>
EAC8 43EB D3EF DB98 CC77 2FAD A5CD 6035 332F A671

_______________________________________________
Concurrency-interest mailing list
Concurrency-interest at cs.oswego.edu<mailto:Concurrency-interest at cs.oswego.edu>
http://cs.oswego.edu/mailman/listinfo/concurrency-interest

-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://cs.oswego.edu/pipermail/concurrency-interest/attachments/20170708/ecfb31af/attachment-0001.html>


More information about the Concurrency-interest mailing list