2

Consider this example. We're having:

int var = 0;

Thread A:

System.out.println(var);
System.out.println(var);

Thread B:

var = 1;

The threads run concurrently. Is the following output possible?

1
0

That is, the original value is read after the new value was read. The var isn't volatile. My gut feeling is that it's not possible.

Oliv
  • 10,221
  • 3
  • 55
  • 76
  • writing and reading variables from different threads without synchronization is just wrong. – Alexei Kaigorodov Nov 24 '20 at 12:49
  • @AlexeiKaigorodov Let's say I don't care about reading a stale value, even for a very long time. I only care about reading a new value and then the old value again, in the same thread. – Oliv Nov 24 '20 at 13:01
  • May I ask what the motivation behind the question is? It seems to me as if there is some cunfusion about something but you haven't really explained it. – akuzminykh Nov 24 '20 at 16:44
  • @akuzminykh I added my use-case to the question. But let's stick to the original question. – Oliv Nov 25 '20 at 08:21
  • @Oliv Now that I know your use case this is simply a question about synchronization. You need to synchronize the access on `var` as Alexei has pointed out. If you care about performance in that regard, check out the Java section in here: [Double-checked locking](https://en.wikipedia.org/wiki/Double-checked_locking). If you don't want to stick to the original question, I'd edit my answer. ;) – akuzminykh Nov 25 '20 at 10:53
  • @akuzminykh I'll probably delete the use case. The question itself is interesting and can be answered. My use case has multiple assignments in thread B so it's a more complex issue. I'd like to see an answer citing JLS rules that guarantee the behavior, I'll mark such answer as correct. I was trying to do that myself, but I'm not able to. Maybe I'll try again to write such answer. – Oliv Nov 25 '20 at 13:21
  • 2
    Removing the use case was bad move, as the use case is different to your question. The two subsequent `System.out.println(var);` statements can not perceive the old value after the new, because `println` uses `synchronized` internally, with a broader side effect than necessary. That’s an implementation detail, but one that exists for a quarter century now. – Holger Nov 25 '20 at 14:13
  • 1
    your use-case was fabulous, imho; and would have touched two points from the JLS that are by far not trivial: `program order`, `happens before` and `happens before consistency`, the latter would prove that `return var` can return a zero, even if you entered that `if` block. – Eugene Nov 25 '20 at 15:30

4 Answers4

3

You are using System.out.println that internally does a synchronized(this) {...} that will make things a bit more worse. But even with that, your reader thread can still observe 1, 0, i.e. : a racy read.

I am by far not an expert of this, but after going through lots of videos/examples/blogs from Alexey Shipilev, I think I understand at least something.

JLS states that :

If x and y are actions of the same thread and x comes before y in program order, then hb(x, y).

Since both reads of var are in program order, we can draw:

                (po) 
firstRead(var) ------> secondRead(var)
// po == program order

That sentence also says that this builds a happens-before order, so:

                (hb) 
firstRead(var) ------> secondRead(var)
// hb == happens before

But that is within "the same thread". If we want to reason about multiple threads, we need to look into synchronization order. We need that because the same paragraph about happens-before order says:

If an action x synchronizes-with a following action y, then we also have hb(x, y).

So if we build this chain of actions between program order and synchronizes-with order, we can reason about the result. Let's apply that to your code:

            (NO SW)                    (hb)
write(var) ---------> firstRead(var) -------> secondRead(var)

// NO SW == there is "no synchronizes-with order" here
// hb    == happens-before

And this is where happens-before consistency comes at play in the same chapter:

A set of actions A is happens-before consistent if for all reads r in A, where W(r) is the write action seen by r, it is not the case that either hb(r, W(r)) or that there exists a write w in A such that w.v = r.v and hb(W(r), w) and hb(w, r).

In a happens-before consistent set of actions, each read sees a write that it is allowed to see by the happens-before ordering

I admit that I very vaguely understand the first sentence and this is where Alexey has helped me the most, as he puts it:

Reads either see the last write that happened in the happens-before or any other write.

Because there is no synchronizes-with order there, and implicitly there is no happens-before order, the reading thread is allowed to read via a race. and thus get 1, than 0.


As soon as you introduce a correct synchronizes-with order, for example one from here

An unlock action on monitor m synchronizes-with all subsequent lock actions on...

A write to a volatile variable v synchronizes-with all subsequent reads of v by any thread...

The graph changes (let's say you chose to make var volatile):

               SW                       PO
write(var) ---------> firstRead(var) -------> secondRead(var)

// SW == there IS "synchronizes-with order" here
// PO == happens-before

PO (program order) gives that HB (happens before) via the first sentence I quoted in this answer from the JLS. And SW gives HB because:

If an action x synchronizes-with a following action y, then we also have hb(x, y).

As such:

               HB                       HB
write(var) ---------> firstRead(var) -------> secondRead(var)

And now happens-before order says that the reading thread will read the value that was "written in the last HB", or it means that reading 1 then 0 is impossible.


I took the example jcstress samples and introduced a small change (just like your System.out.println does):

@JCStressTest
@Outcome(id = "0, 0", expect = Expect.ACCEPTABLE, desc = "Doing both reads early.")
@Outcome(id = "1, 1", expect = Expect.ACCEPTABLE, desc = "Doing both reads late.")
@Outcome(id = "0, 1", expect = Expect.ACCEPTABLE, desc = "Doing first read early, not surprising.")
@Outcome(id = "1, 0", expect = Expect.ACCEPTABLE_INTERESTING, desc = "First read seen racy value early, and the second one did not.")
@State
public class SO64983578 {

    private final Holder h1 = new Holder();
    private final Holder h2 = h1;

    private static class Holder {

        int a;
        int trap;
    }

    @Actor
    public void actor1() {
        h1.a = 1;
    }

    @Actor
    public void actor2(II_Result r) {
        Holder h1 = this.h1;
        Holder h2 = this.h2;
        
        h1.trap = 0;
        h2.trap = 0;

        synchronized (this) {
            r.r1 = h1.a;
        }

        synchronized (this) {
            r.r2 = h2.a;
        }

    }

}

Notice the synchronized(this){....} that is not part of the initial example. Even with synchronization, I still can see that 1, 0 as a result. This is just to prove that even with synchronized (that comes internally from System.out.println), you can still get 1 than 0.

Eugene
  • 117,005
  • 15
  • 201
  • 306
1

When the value of var is read and it's 1 it won't change back. This output can't happen, neither due to visibility nor reorderings. What can happen is 0 0, 0 1 and 1 1.

The key point to understand here is that println involves synchronization. Look inside that method and you should see a synchronized there. These blocks have the effect that the prints will happen in just that order. While the write can happen anywhen, it's not possible that the first print sees the new value of var but the second print sees the old value. Therefore, the write can only happen before both prints, in-between or after them.

Besides that, there is no guarantee that the write will be visible at all, as var is not marked with volatile nor is the write synchronized in any way.

akuzminykh
  • 4,522
  • 4
  • 15
  • 36
  • @Holger It's based on the fact that we have two prints/reads of `var` by A and one write to `var` by B. Assuming that the write is visible to A, it happens either before the prints, in-between or after, which leads to the possibilities I've mentioned. If the write is not visible, then it's just `0 0`. If there were reorderings, which is not the case as `println` has `synchronized` in it, then there is still no serialization of events that would result A to magically print `0` after it reads `1`. What have I missed? – akuzminykh Nov 25 '20 at 14:32
  • 2
    First of all, an answer should include the reasoning. And “assuming that the write is visible to A” is missing the point, as the question is specifically about the absence of visibility, as the variable is not `volatile`. Since in that case, no guarantees are made, a read may happen to perceive an updated value, but that does not retroactively establish a happens-before relationship. Further “serialization of events” is not a thing. Only the `synchronized` within the `println` affects this specific example, so it’s irresponsible not to mention it in the answer. – Holger Nov 25 '20 at 16:46
  • 1
    `1 0` can’t happen as long as the internals of `println` prevent it. But generally, without additional synchronization primitives, reading a non-volatile variable that might be modified by a different thread is a racy read. And for two subsequent racy reads there are no visibility guarantees and what looks like the subsequent read may perceive an older value than the previous read. You’re not saying anything about happens-before, but you *should*, as only those relationships, as defined by [the specification](https://docs.oracle.com/javase/specs/jls/se15/html/jls-17.html#jls-17.4), matter. – Holger Nov 25 '20 at 17:40
  • 1
    While doing more research on just that topic and similar examples I've actually found the exact duplicate: [Reordering of reads](https://stackoverflow.com/q/37240208/12323248). It blows my mind that `1 0` would be possible if there was no synchronization within `println`. Thank you again for pointing out my misunderstandings, @Holger. – akuzminykh Nov 25 '20 at 18:46
1

I think what is missing here is the fact that those threads run on actual physical cores and we have few possible variants here:

  1. all threads run on the same core, then the problem is reduced to the order of execution of those 3 instructions, in this case 1,0 is not possible I think, println executions are ordered due to the memory barriers created by synchronisation, so that excludes 1,0

  2. A and B runs on 2 different cores, then 1,0 does not look possible either, as as soon the core that runs thread A reads 1, there is no way it will read 0 after, same as above printlns are ordered.

  3. Thread A is rescheduled in between those 2 printlns, so the second println is executed on a different core, either the same as B was/will be executed or on a different 3rd core. So when the 2 printlns are executed on a different cores, it depends what value does 2 cores see, if var is not synchronised (it is not clear is var a member of this), then those 2 cores can see different var value, so there is a possibility for 1,0.

So this is a cache coherence problem.

P.S. I'm not a jvm expert, so there might be other things in play here.

  • 1
    The thing you’re overlooking is the JIT compiler/optimizer that may transform the code to something that does not even remotely look like what you’ve written in the source code. It’s pointless to discuss how the code may be affected by the CPU architecture when you don’t know how the actual code looks like. Cache coherence is the least problem. – Holger Nov 26 '20 at 11:08
  • JIT can do whatever it likes, apart from breaking memory model. – Alex Revetchi Nov 26 '20 at 11:50
  • 1
    Now, you are close to the correct answer. [*The memory model*](https://docs.oracle.com/javase/specs/jls/se15/html/jls-17.html#jls-17.4) specifies what is a legal execution, not the stuff you wrote about “actual physical cores” or thread scheduling. – Holger Nov 26 '20 at 11:55
  • So my point was if var is not affected by println() synchronisation, the actual physical execution matters, as writing to var and var value visibility can vary in different circumstances, so that is the grey jvm area for me. – Alex Revetchi Nov 26 '20 at 12:05
  • 1
    As said, if you don’t understand what the JIT is allowed to do or not, there is no point in discussing how the JIT’s result will interact with the physical architecture. – Holger Nov 26 '20 at 12:20
  • Well so far I do not disagree with you and I'm trying to delve a bit more in to it. But again as I reread the question, the author is asking if the 1,0 is possible. Then we see things explained from the point if few of JLS, but is that the only cause of 1,0 possibility? Then see next authors comment: "Let's say I don't care about reading a stale value, even for a very long time. I only care about reading a new value and then the old value again, in the same thread", so it is quite specific about same thread and the ordering of the two reads in time, so JIT is out of play at that stage. – Alex Revetchi Nov 26 '20 at 13:54
  • As well just noticed 3 extra comments under the question that is easy to miss and needs expanding, where, the author changes/concretises the question. So then in that context cpu arch is out of play. – Alex Revetchi Nov 26 '20 at 14:06
  • 1
    The JLS is the only relevant thing, as when a hardware architecture allows certain things the JLS forbids, it’s the duty of the JVM developers to prevent this from happening. For example, the JLS forbids speculative execution to turn a condition into a self-fulfilling prophecy (“out of thin air” values). So for architectures having such speculative execution (Alpha AFAIK), the JVM developers have to limit its effects to conform to the JLS. So, in the end, it doesn’t matter for the Java developer if a behavior is caused by JIT or hardware, as it only happens because the JLS allows it. – Holger Nov 26 '20 at 14:13
0

Adding to the other answers:

With long and double, writes may not be atomic so the first 32 bits could become visible before the last 32 bits, or viceversa. Therefore completely different values could be output.

spongebob
  • 8,370
  • 15
  • 50
  • 83