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.