0

Consider the following program:

import java.util.concurrent.TimeUnit;
public class StopThread {
    public static boolean stopRequested;

    public static void main(String[] args) throws InterruptedException {
        Runnable task = new Runnable() {
            @Override
            public void run() {
                int i = 0;
                while (!stopRequested) {
                    i++;
                    System.out.println(i);
                }
                System.out.println("Stopping the thread!!");
            }
        };
        Thread backgroundThread = new Thread(task);

        backgroundThread.start();
        TimeUnit.SECONDS.sleep(5);
        stopRequested = true;
    }
}

  • Here the stopRequested is not declared as volatile - so ideally the thread backgroupdThread must not stop - and execute endlessly
  • But when running this locally - the thread backgroundThread is gracefully shutting down with the message: "Stopping the thread!!".

Are all the updates by the main() thread to the shared variable stopRequested visible to the other threads? even without the use of volatile keyword?

b.s
  • 2,409
  • 2
  • 16
  • 26
theutonium.18
  • 483
  • 2
  • 7
  • 2
    _with_ volatile you get guarantees, _without_ you don't. The fact that you see or not something in your example, does not mean it is correct. `main` is unrelated to this – Eugene Jul 03 '21 at 20:35
  • 1
    I think printing causes the threads to synchronise. – khelwood Jul 03 '21 at 20:36
  • Possibly related: [Loop doesn't see value changed by other thread without a print statement](https://stackoverflow.com/q/25425130) – Pshemo Jul 03 '21 at 20:40
  • @Eugene this famous code is from the Effective Java book "Item 78" - I completely agree with you that this is just a case of "Guarantee" but here is another example - where this behaviour is very much explicit - ie If I do not use volatile - even with the main() the inter-thread communication is not happening.. – theutonium.18 Jul 03 '21 at 20:53
  • public class VolatileDemo{private static boolean flag=false;public static void main(String[]args)throws InterruptedException{new Thread(new Runnable(){@Override public void run(){for(int i=1;i<=20000;i){System.out.println("value = "i);} flag=true;System.out.println("Status of flag changed: "flag);}}).start();new Thread(new Runnable(){@Override public void run(){int i=1;while(!flag){i;} System.out.println("Seen the value of flag "i);}}).start();Thread.sleep(3000);flag=true;}} – theutonium.18 Jul 03 '21 at 20:58
  • @Eugene Just wanted to make sure if there are any formal rules around the fact that **why the volatile is not needed in the case-1 but in the case-2 this claim becomes very true and volatile is needed**. – theutonium.18 Jul 03 '21 at 21:00
  • @theutonium.18 where is that "here"? The examples where this could break and how, have changed quite a lot with java versions. Even `jcstress` ( that is used to specifically prove these kind of things ) is not always correct in its examples with latest jvms. – Eugene Jul 03 '21 at 21:01
  • 1
    If you omit volatile where is necessary for correctness (or omit any synchronization needed for correctness), then your code may appear to work, up to the point at which it doesn't. If you're particularly unlucky, your code will work for a long time. Worst-case is that it works just fine up until you've shipped many copies. – iggy Jul 03 '21 at 21:13
  • imo you have an answer that nails it. the point is, `JLS` does not treat `volatile` any different with or without `main`, so don't judge your code based on that. – Eugene Jul 03 '21 at 21:55

2 Answers2

4

The Java Language Specification does not guarantee this outcome.

In the absence of a synchronization action (such as a volatile write with a subsequent read), the write does not happen-before the read, and is therefore not guaranteed to be visible.

That is, the read may see the old value or it may see the new one; either outcome is permitted by the Java Memory Model.

To see how narrow the gap is, try removing the printing from the loop:

                while (!stopRequested) {
                    i++;
                }

Executed on

openjdk version "14" 2020-03-17
OpenJDK Runtime Environment (build 14+36-1461)
OpenJDK 64-Bit Server VM (build 14+36-1461, mixed mode, sharing)

this code does not terminate. The salient difference is that the loop body become less complex, causing the JIT to apply additional optimizations :-)

As you can see, the behavior of incorrectly synchronized programs is unpredictable, and can change given the slightest provocation. If you want to write robust multi threaded code, you should therefore prove your code correct with respect to the specification rather than relying on testing.

meriton
  • 68,356
  • 14
  • 108
  • 175
  • This is dangerous way to formulate it 'The write doesn't happen before the read". It is better to formulate it like this: If the read sees a particular write, then there is (or is not) a happens before edge between that write and the read. – pveentjer Jul 04 '21 at 03:14
  • The behavior or correctly synchronized programs also isn't required to be deterministic btw. Every run of a concurrent program can give a different outcome. – pveentjer Jul 04 '21 at 03:17
  • @pveentjer: Why do you find it "dangerous", and why do you find your phrasing "better"? – meriton Jul 04 '21 at 03:28
  • ... and yes, I know that programs can be non-deterministic even if they are correctly synchronized. Hell, they can be non-deterministic even if they're single threaded. But it is possible for a concurrent program to behave in a deterministic way, and correct synchronization is often necessary to achieve that. – meriton Jul 04 '21 at 03:39
  • Because the reason reasoning is backwards. There could be a write of a value that is observed by a read, without a happens-before edge between these 2 actions (data race). So the happens before doesn't control that the write happens first and then the read will happen. It just means that if the read observes a write, AND there is a happens before edge, then there is a causal relation between the write and the read and the reading thread should see everything that happened so far in the writing thread. – pveentjer Jul 04 '21 at 03:40
  • PS: I ran into problems with the wording you are using above because I used the same. Only once I started to use the wording I use above, it started to make a lot more sense. – pveentjer Jul 04 '21 at 03:43
  • I do not believe your statement about deterministic behavior is valid in this case. A correctly synchronized (in case of JMM it will be DRF-sc); is allowed to be non deterministic. Claiming anything else is just a source of confusion. For more info see: https://en.wikipedia.org/wiki/Consistency_model#Sequential_consistency – pveentjer Jul 04 '21 at 03:50
  • The JLS writes: "Two actions can be ordered by a happens-before relationship. If one action happens-before another, then the first is visible to and ordered before the second." That is, if we want an action to be visible to another, we must ensure it happens-before the other. I don't see anything "backwards" about that reasoning? – meriton Jul 04 '21 at 04:00
  • In particular, I never claimed that happens-before "controls" anything. This would be absurd, like claiming that the passage of time "controls" my processor. Two actions may or may not be in a happens-before relationship - but if they are, it implies visibility, and if we want visibility, we should therefore establish happens-before. – meriton Jul 04 '21 at 04:01
  • As for deterministic: Notice that I write about deterministic *behavior* while you seem to be talking about deterministic *executions*. That's not the same thing. We want behavior to be consistent across all possible executions, and we can achieve this by ensuring that all possible executions exhibit the same externally visible behavior. – meriton Jul 04 '21 at 04:09
  • Due to concurrency, each execution of a program can lead to different behavior. So you still end up with non deterministic behavior. This is not a bad thing for a concurrent program; it is inherent to its nature. – pveentjer Jul 04 '21 at 04:12
  • @meriton thanks for the response - wanted to ask - any particular reasons why simply removing the print statement made it work - I understand it may not have anything directly to do with concurrency - but in terms of other mechanisms like - reaching out to Main Memory etc might be responsible? – theutonium.18 Jul 04 '21 at 05:33
  • @meriton: There are 3 critical parts to the JMM: ordering, visibility and atomicity. Visibility is mostly a compiler concern to prevent the compiler to optimize-out a load/store. Atomicity is a field access doesn't get messed up like e.g. torn read/write. But the bread and butter of memory models is ordering because this is where you get the guarantee that if a->b, then 'b' will should see at least all state changes before and including 'a'. – pveentjer Jul 04 '21 at 13:05
  • @theutonium.18: What probably happened was that the code for the loop body became small enough to be fully inlined and therefore JIT-compiled in one go, allowing the JIT to keep the field value in a CPU register (or the thread stack) rather than loading it anew from main memory every time. The JIT is allowed to do this because visibility is not guaranteed in the absence of a synchronization action. – meriton Jul 04 '21 at 14:05
  • Loads and stores don't need to come from main memory, even not for volatile because caches are always coherent. Thinking in term of main memory access is flawed: this is not how CPUs work. – pveentjer Jul 04 '21 at 16:11
  • Look, if you feel the need to nitpick every sentence I write, why don't you write an answer of your own? If you do, you'll likely find that, in order to keep an answer at a reasonable length, one has to focus on the things that are directly relevant to OP's question. The existence of CPU caches is not among these things, because their existence does not affect the phenomena we discuss here. – meriton Jul 04 '21 at 16:30
0

You haven't fully grokked the concept of the Java Memory Model (JMM).

The JMM works on the basis of so-called Happens-Before/Happens-After relationships. This is how it works:

  • Whenever any thread reads a field, we call this 'observing'.^1
  • If there is a happens-before/happens-after relationship between line X (happens before) and line Y (happens after), then the JVM guarantees you that you cannot observe the value of a field as it was before X ran, from Y. That is the one and only guarantee that the entire JMM gives you. It makes zero guarantees in any other fashion: It says nothing about what Y's writes do for X (in particular, X may also see Y's writes, which is weird, because didn't Y run after? - and also, it makes no guarantees when there is no HB/HA either: Then Y may see the state as it was before X, or after X, either one can occur!)
  • HB/HA and actual time are completely unrelated. If you use a clock to determine that line B occurred after line A, that is no guaranteed that an HB/HA relationship exists between these two, and as a consequence, any writes to fields caused by A are not neccessarily observable in B. Similarly, if line B and A do have an HB/HA relationship, you are guaranteed that you cannot observe any field in a state as it was before A ran from B, but, you don't actually get any guarantee that B will physically (as in, as per the clock) runs after A. Usually it will have to in order to be able to have B observe the changes A made, but if B isn't actually checking anything A wrote, then there is no need for the JVM and the CPU to be careful and the 2 statements can run in parallel, or B can even run before A, HB/HA relationship be damned.
  • The guarantee is not a two-way street! Yes, if B 'happens after' A, then you get the guarantee that you cannot observe the state of a field as it was before A. But the reverse is simply not true! If A and B have no HB/HA relationship at all, you get no guarantees. You get instead what I like to call the evil coin.

Whenever there is no HB/HA relationship and the JVM reads a field for you, the JVM will take out the evil coin from its pouch of doom and flip it. Tails, and you get the state as it was before A wrote to it (e.g. you get a local cached copy). Heads, and you get the synchronized version instead.

The coin is evil, in that it doesn't land heads/tails in a nice arbitrary 50/50 chance either way fashion. Nono. It'll land so that your code works great every time you run it today, and every time the test suite runs on the CI server this entire week. And then after it hits the production servers it still lands the way you want it to every time. But 2 weeks from now just as you are giving the demo to the huge potential new customer?

Then it decides to reliably flip the other way on you.

The JMM gives JVMs that power. This seems utterly insane but it does so to let the JVM optimize as much as it does. Any further guarantees would significantly reduce the actual speed of operation of a JVM.

So, you must not let the JVM flip that coin. Establish HB/HA whenever you communicate between 2 threads by way of field writes (almost everything is a field write eventually, keep that in mind!)

How do you establish HB/HA? Many, many ways - you can search for it. The most obvious ones:

  • The natural HB/HA: Any line that is 'below' another and runs in the same thread trivially establishes HB/HA. I.e. x = 5; y = x;, just like that in a single thread? That x read obviously will see your write.
  • thread starting. thread.start(); HBs before the first line inside that thread.
  • synchronized. Hopping out of a synchronized(x){} block is guaranteed to HB before the first line of code in the synchronized block of another thread enters a block (on the same x!)
  • volatile access similarly establishes HB/HA though it's very hard to figure out which line actually comes first (it basically says that one of em HBs before the other but which one?), so keep that in mind.

Your code has no HB/HA at all, so the JVM is flipping the evil coin. You can't draw any conclusions here. That stopRequested may update immediately, it may update next week, it may never update, or it may update in 5 seconds. A JVM that checks the phase of the moon before deciding which of those 4 options to do is 100% a valid java implementation. The only solution is not to let the VM flip that coin, so, establish HB/HA with something.

[1] Computers try to parallelize and apply all sorts of utterly bizarre optimizations. This affects timing (how long things take), and there is no (easy) way to give you timing guarantees, so the JMM simply does not do this. In other words, you can 'see' all sorts of bizarreness if you start timing things out and/or attempting to register the CPU timer (System.nanoTime) and trying to log the order of things, but if you actually try, do note that almost all ways to log events will cause synchonization and therefore completely invalidate your test. The point is, if you do it right you can observe things running in parallel and even completely out of order. The guarantees aren't about any of this, the guarantees are about reading fields, only.

rzwitserloot
  • 85,357
  • 5
  • 51
  • 72