2

I wrote down this code:

public class Main {

    private boolean stopThread = false;
    private int counter = 0;

    public static void main(String[] args) throws InterruptedException {
        final Main main = new Main();

        new Thread(() -> {
            try {
                System.out.println("Start");
                Thread.sleep(100);
                System.out.println("Done");
                main.stopThread = true;
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        }).start();

        new Thread(() -> {
            while (!main.stopThread) {
                main.counter++;
            }
            System.out.println(main.counter);
        }).start();
        System.out.println("End");
    }
}

and when I run it, the while loop will be running on forever. I was struggling with this a bit, and I got confused what kind of optimalization JIT applied to this code.

First of I was thinking that this is a problem with visibility of stopThread variable, but even if it is a true the while loop should stop a little bit later than I assigned stopThread to true (when CPU cache from 1st thread got flushed to the main memory), so it cannot be the case. It looks like the JIT hardcoded false to stopThread variable, and if it is true, why this variable is not refreshed periodically somehow on runtime?

Obviously, the volatile keyword fixed that, but it is not answering me to that question because volatile can ensures visibility as well as prevent JIT from number of optimalizations.

Even more, when I change the sleep time to 1ms, the second thread will terminate properly, so I'm pretty sure that this is not about variable visibility.

UPDATE: It is worth to mention that I'm getting non-zero value from the counter when the sleep time is set to 1-10 ms.

UPDATE 2: Additionaly, I can say that -XX:+PrintCompilation shows that in case when the sleep time is set to 100 ms the while loop gets compiled, and the On Stack Replacement happend.

UPDATE 3: Probably this is what I was looking for: https://www.youtube.com/watch?v=ADxUsCkWdbE&feature=youtu.be&t=889. And as I thought - this is one of "optimalization" performed by JIT, and the way to prevent it is to specify variable as volatile, or put loadloadFence as the first line in while loop.

ANSWER: As @apangin said:

This optimization is Loop invariant hoisting. JIT is allowed to move the load of stopThread out of the loop, since it may assume that non-volatile field does not change externally, and JIT also sees that stopThread does not change inside the loop.

Martin
  • 129
  • 1
  • 6
  • 1
    This optimization is [Loop invariant hoisting](https://en.wikipedia.org/wiki/Loop-invariant_code_motion). JIT is allowed to move the load of `stopThread` out of the loop, since it may assume that non-volatile field does not change externally, and JIT also sees that `stopThread` does not change inside the loop. – apangin Dec 22 '19 at 11:23
  • @apangin and probably this is why the `loadLoadFance()` also works, without need to specify the variable as `volatile`. And perhaps the `volatile` is still needed to prevent having stale value in CPU cache. Thank you. – Martin Dec 22 '19 at 11:35
  • @apangin, you could add that as an answer because the current answers don't mention the exact optimization that takes place here. – Mick Mnemonic Dec 22 '19 at 12:22

3 Answers3

5

The reason isn't JIT optimization. Your code is doing unsynchronized access to a shared variable, stopThread (let's call it "flag").

Essentially there is a race condition between the thread that is setting the flag to a truthy value and the other thread that is checking the value. If the flag is set true before the loop is entered, the code completes. If not (race lost), the loop will continue indeterminately long, because the CPU cache holds the falsey value. When the flag is volatile, its value is read from main memory instead of the CPU cache and the loop eventually finishes right after the flag-setting thread is done with sleeping.

Mick Mnemonic
  • 7,808
  • 2
  • 26
  • 30
  • But If it was a true: `If the flag is set true before the loop is entered, the code completes.` I wouldn't get any value from `counter`, but I'm getting non-zero value from it when the `sleep` time is set to 1ms. – Martin Dec 21 '19 at 22:39
  • I tested with your code using `sleep(1)` and with/without `stopThread` being `volatile` and the results seem quite the same. `Thread.sleep()` is a `native` method, meaning that the implementation depends on the OS (and processor architecture), but my testing with Windows/Intel i7 seems to imply that the CPU cache doesn't get used when you specify the minimum thread sleep time. It could be that they even treat `Thread.sleep(0)` and `Thread.sleep(1)` as "no sleep" in Windows as an optimization of some sort. Using a sleep time of 2 ms always hangs the application. – Mick Mnemonic Dec 21 '19 at 23:58
  • 1
    In short, the phenomenon seems to have more to do with CPU caches and Java's memory model than JIT. – Mick Mnemonic Dec 22 '19 at 00:03
  • I tested it on 10ms `sleep` and the application hangs in most of tries, but not **always**. But I found great talk and probably this is what I was looking for: https://youtu.be/ADxUsCkWdbE?t=889. And as I thought - this is one of "optimalization" performed by JIT, and the way to prevent it is to specify variable as `volatile`. – Martin Dec 22 '19 at 00:15
  • Of course it is allowed to question any answer, but sometimes I have the idea that people ask a question and then start defending the initial view that they held when they get an answer that doesn't fit in their pre-defined model. If you're not even going to explore or look into the matter from a different view, then what's the point asking the question in the first place? Why would these two viewpoints even be exclusive of each other? – Maarten Bodewes Dec 22 '19 at 02:36
  • This is not a race condition. Just a cache problem. – Thorbjørn Ravn Andersen Dec 22 '19 at 07:01
3

I think you are coming at this problem from the wrong direction.

What you have is a program with an unspecified behavior with respect to memory visibility. If two threads are reading and writing a shared variable, it must be done in specific ways to guarantee that one thread sees th other threads writes. You need to use synchronized or a volatile variable to ensure that the write happens before the read.

If the happens before is not present, the JVM is allowed to assume that only the current thread is accessing / updating the variable. What the JIT compiler actually does depends on all sorts of things, and it is not generally helpful to go into details.

It is also worth noting that volatile is not necessarily the best (most efficient) fix for a memory visibility flaw.

References:

Stephen C
  • 698,415
  • 94
  • 811
  • 1,216
  • Seems, that `VarHandle.loadLoadFance()` can also fix that problem which I believe is more efficient than `volatile` variable. – Martin Dec 22 '19 at 11:01
0

without any synchronization or volatile keyword, it's allowed to not read stopThread variable from memory, it can read it from cache (or even from register) where it put it first time.

What exactly it did and why it worked with sleep(1), we can only speculate. My guess is that first thread wrote stopThread = true before second thread read it. Maybe try starting looping thread first.

Alpedar
  • 1,314
  • 1
  • 8
  • 12
  • If the second part of your answer was a true, I would not get any result from the `counter`, but I'm getting non-zero value from it. – Martin Dec 21 '19 at 22:38
  • 1
    Maybe first few iterations are executed different than other iterations (JVM can interpret code and later JIT it run it's compiled version) and MAYBE (that's wild guess) it's what happened. – Alpedar Dec 21 '19 at 22:54
  • I agree with @Alpedar on this one. The JVM will optimize/recompile code even after a code has already been compiled/executed. That is the main reason why performance tests of java code are not accurate when done once (hence why most test-setups uses looping, where they skip the first chunk of iterations te exclude/skip the 'optimizing phase'). A verry big change it is also responsible for the 'unexplainable' results. – n247s Dec 21 '19 at 23:00