javamultithreadingconcurrencyatomicinterleave

Can Atomic values change during an "&&" operation?


I am aware of the next scenario: (Weird formatting, I know)

private final AtomicBoolean aBoolean = new AtomicBoolean(true);

public void doSomething() {
    if (
        aBoolean.get()                       // line A
            &&                               // line B
        aBoolean.compareAndSet(true, false)  // line C
        ) {
        System.out.println("Was true!")
    }
}

If thread #1 and thread #2 enter doSomething() at exactly the same time this will happen:

  1. Thread #1 and thread #2 will read aBoolean.get() as being == "true" simultaneously.

  2. Both will execute the "&&" operator.

  3. CMPXCHG instruction kicks in for both threads simultaneously:

    3.1 LOCK prefix is used natively

    3.2 Either thread #1 or #2 arrives first, winning the race.

    3.3 Winning thread compares (is aBoolean == true ?) this will return "true", hence aBoolean will be set to "false".

    3.4 aBoolean is now false.

    3.5 Losing thread compares (is aBoolean == true ?) this will return "false", hence short-circuiting any further operations.

  4. winning thread will print "Was true!".

Under the perspective of the "losing" thread the first aBoolean.get() in "line A" was .... let's say... a "lie".

Now under the assumption that executions can happen in between operators, as it just did in the example above, let's add a second method for a second scenario:

public void unluckySet() {
    aBoolean.set(false);
}

Let's say Thread #3 just so happens to arrive and execute unluckySet() at precisely the exact moment that our "winning thread" arrived at "line B" where the "&&" is executing.

If winning thread went as far as to reach "line B" it means it reached "line A" with aBoolean being "true".

My questions are:

Will CMPXCHG read the updated value correctly as being "false"?, meaning the .set() is also held by the same lock as the compareAndSet().

In concurrency and between threads:

Do operators ("&&", "||", "==", "=", maybe even "return;"??) happen at any nano second, OR are they interleaved with executions (";") so that both END in an interleaved manner, preventing possible collisions?


Solution

  • The Java memory model is sequential consistency in the absence of data races (which your program doesn't have). This is very strong; it says that all reads and writes in your program form a total order, which is consistent with program order. So you can imagine that reads and writes from different threads simply get interleaved, or shuffled together, in some fashion - without changing the relative ordering of actions made from the same thread as each other.

    But for this purpose, every action is a separate element in this order. So just by the mere fact that aBoolean.get() and aBoolean.compareAndSet() are two actions and not one action, it is possible for any number of other actions by other threads to take place in between them.

    It doesn't matter whether those actions are part of a single statement, or different statements; or what kind of expression they appear in; or what operators (if any) are between them; or what computations the thread itself may or may not be doing around them. There is no way in which two actions can be "so close together" that nothing else can happen in between, short of replacing them by a single action defined to be atomic by the language.


    At the level of the machine, a very simple way this can happen is that, since aBoolean.get() and aBoolean.compareAndSet() are almost certainly two different machine instructions, an interrupt may arrive in between them. This interrupt could cause the thread to be delayed for any amount of time, during which other threads could do anything they wish. So it is entirely possible that threads #1 and #2 are both interrupted in between their get() and compareAndSet(), and that thread #3 executes its set in the meantime.

    Caution: Reasoning about how a particular machine might work is often useful for understanding why undesired behavior is possible, as in the previous paragraph. But it is not a substitute for reasoning about the formal memory model, and should not be used to try to argue that a program must have its desired behavior. Even if a particular machine you have in mind would do the right thing for your code, or you can't think of a way in which a plausible machine would fail, that does not prove that your program is correct.

    So trying to say "oh, the machine will do a lock cmpxchg and so blah blah blah and everything works" isn't wise; some other machine you haven't thought of might work in a totally different fashion, that still complies with the abstract Java memory model but otherwise violates your x86-based expectations. In fact, x86 is a particularly poor example for this: for historical reasons, it provides a rather strong set of instruction-level memory ordering guarantees that many other "weakly ordered" architectures do not, and so there may be many things that Java abstractly allows but that x86 in practice won't do.