0

I am totally puzzled with the two samples.

public class VTest {
    private static /*volatile*/ boolean leap = true;
    public static void main(String[] args) throws InterruptedException {
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                while (leap) {

                }
            }
        });
        t2.start();
        Thread.sleep(3000);
        leap = false;
    }
}

In this case, t2 is not able to stop, as leap was stored locally so that t2 can't access the leap updated in main thread.

public class VTest2 {
    private static int m = 0;
    public static void main(String[] args) throws InterruptedException {
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10000; ++i) ++m;
            }
        });
        t2.start();
        for (int i = 0; i < 10000; ++i) ++m;
        Thread.sleep(3000);
        System.out.println(m);
    }
}

But, in this case, the m is always be 20000, why isn't 10000?

Any answer will be appreciated.

Jardaliao
  • 5
  • 1
  • Trying to make assumptions without proper synchronization will mostly fail. In my case I never got a 20000 when I ran this. I got random results 18846, 19256 and this is because there is no technique used to guarantee memory visibility between these two threads – Ioan M Feb 24 '21 at 16:23

2 Answers2

0

It's not really a matter of "when". Because of the way that m is declared, the two threads have no reason to believe that it needs to consider the value in main memory.

Consider that ++m is not an atomic operation, but is rather:

  • A read
  • An increment
  • A write

Because the thread doesn't know it needs to read from or flush to main memory, there is no guarantee as to how it is executed:

  • Perhaps it reads from main memory each time, and flushes to main memory each time
  • Perhaps it reads from main memory just once, and doesn't flush to main memory when it writes
  • Perhaps it reads from/writes to main memory on some iterations of the loop
  • (...many other ways)

So, essentially, the answer is that there is no guarantee that the value is read from or written to main memory, ever.

If you declare m as volatile, that gives you some guarantees: that m is definitely read from main memory, and definitely flushed to main memory. However, because ++m isn't atomic, there is no guarantee that you get 20000 at the end (it's possible it could be 2, at worst), because the work of the two threads can intersperse (e.g. both threads read the same value of m, increment it, and both write back the same value m+1).

To do this correctly, you need to ensure that:

  1. ++m is executed atomically
  2. The value is guaranteed to be visible.

The easiest way of doing this would be to use an AtomicInteger instead; however, you could mutually synchronize the increments:

synchronized (VTest2.class) {
  ++m;
}

You then also need to synchronize the final read, in order to ensure you are definitely seeing the last value written by t2:

synchronized (VTest2.class) {
  System.out.println(m);
}
Andy Turner
  • 137,514
  • 11
  • 162
  • 243
0

In this case, t2 is not able to stop, as leap was stored locally so that t2 can't access the leap updated in main thread.

That's not really the case: the leap variable was not stored "locally" by the thread. It's still a shared static variable. However, because it is not marked as volatile, and there is no synchronization happening whatsoever, the JVM (the JIT in particular) is free to do optimization to avoid loading it. I believe in this case it is removing the check on the variable.

Note: The second code incrementing m is not thread-safe: try increasing the loop to millions to test that, it will almost never match the expected sum.

M A
  • 71,713
  • 13
  • 134
  • 174