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

Michael Hixson michael.hixson at gmail.com
Thu Jul 6 10:31:58 EDT 2017


Forwarding this reply of mine because I forgot to CC the mailing list.


---------- Forwarded message ----------
From: Michael Hixson <michael.hixson at gmail.com>
Date: Thu, Jul 6, 2017 at 7:24 AM
Subject: Re: [concurrency-interest] Should I avoid compareAndSet with
value-based classes?
To: Andrew Haley <aph at redhat.com>


On Thu, Jul 6, 2017 at 4:20 AM, Andrew Haley <aph at redhat.com> wrote:
> On 06/07/17 11:41, Alex Otenko wrote:
>>
>>> On 6 Jul 2017, at 10:13, Andrew Haley <aph at redhat.com> wrote:
>>>
>>> On 06/07/17 04:59, Michael Hixson wrote:
>>>> AtomicReference and VarHandle are specified to use == in compareAndSet
>>>> (and related) operations [1].  Using == to compare instances of
>>>> value-based classes may lead to "unpredictable results" [2].  Does
>>>> this mean I should avoid using compareAndSet with arguments that are
>>>> instances of value-based classes?
>>>>
>>>> It seems like the documentation clearly tells me "yes, avoid doing
>>>> that" but I'm hoping I misunderstood, or maybe AtomicReference and
>>>> VarHandle are exempt somehow.  Otherwise, how do I implement
>>>> non-broken compareAndSet and updateAndGet for a java.time.Instant
>>>> value for example?
>>>
>>> java.time.Instant stores times that are longer than a JVM word, so
>>> they cannot be CAS'd in a lock-free way unless a factory guarantees
>>> that instances which compare equal also have the property of
>>> reference equality.  j.t.Instant factories in the Java library are
>>> explicitly documented *not* to have this property, so that doesn't
>>> help.
>>
>> That’s not entirely clear.
>>
>> Wouldn’t this loop work:
>>
>> volatile j.t.Instant curr = ...
>>
>> j.t.Instant next = …
>> j.t.Instant tmp = ...
>> do {
>>   tmp = curr;
>>   if (tmp.equal(next)) break;
>> } while(!curr.CAS(tmp, next));
>>
>> // assume curr is equal to next
>
> Something like that, yes.  But it's going to be evil if there are
> several high-frequency writers.  If you're doing all that work in
> order to CAS a timestamp, why not use a synchronized block?  It would
> at least be less prone to the thundering herd, and we'd generate
> pretty decent code for that.  It'd be interesting to compare and
> contrast the two approaches for contended and non-contended cases.

The main reason I reached for AtomicReference is that I thought, "I
want thread-safe updates to this read-heavy value where writes won't
get lost if there's contention -- this sounds like the sort of problem
that java.util.concurrent.atomic solves."

As a minor point, I wanted synchronization on reads to be as
minor/invisible as possible, to affect the readers' behavior as little
as possible (in comparison to their behavior when the value they're
reading a constant value with no synchronization).

But if AtomicReference is simply the wrong tool to use here, I
shouldn't use it.  That's fine.

>
>>> If you want to be able to CAS a reference to a j.t.Instant, you're
>>> going to have to wrap accesses to it in a synchronized block.  This is
>>> a direct consequence of the JVM's inability to CAS multi-word objects.
>>
>> This is confusing.
>>
>> Surely this isn’t talking about CASing a reference? The contents of
>> the object can’t be assumed to have any atomicity properties,
>> whether it is j.t.Instant or not.
>
> I agree.  I'm trying to look at what the OP actually wants to do: I
> assume this is some kind of atomic timestamp, and the OP wants to be
> able to CAS an instance of j.u.Instant.

It was more of a general worry and question.  Should I ever use
AtomicReference with value-based classes?  It sounds like the answer
is no.

But here's some code that illustrates the specific problem I was
trying to solve.  I think it "works" right now, but that it's in
violation of the spec for value-based classes and so possibly not
future-proof, and I think I understand how to fix it now.  Thanks for
the tips.

    class MutableClock {

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

        private final AtomicReference<Instant> instantHolder;
        private final ZoneId zone;

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

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

        ZoneId getZone() {
            return zone;
        }

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

        void add(Duration amountToAdd) {
            // this is the part that uses == and CAS
            instantHolder.updateAndGet(
                  instant -> instant.plus(amountToAdd));
        }

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

-Michael


More information about the Concurrency-interest mailing list