1

Consider this example from GeeksForGeeks:

public class VolatileKeyword {
    private static final Logger LOGGER = Logger.getLogger(VolatileKeyword.class);
    private static int MY_INT = 0;

    public static void main(String[] args) {
        new ChangeListener().start();
        new ChangeMaker().start();
    }

    static class ChangeListener extends Thread {
        @Override public void run() {
            int local_value = MY_INT;
            while (local_value < 5) {
                // (A)
                if (local_value != MY_INT) {
                    LOGGER.info("Got Change for MY_INT : " + MY_INT);
                    local_value = MY_INT;
                }
            }
        }
    }

    static class ChangeMaker extends Thread {
        @Override public void run() {
            int local_value = MY_INT;
            while (MY_INT < 5) {
                LOGGER.log(Level.INFO, "Incrementing MY_INT to " + (local_value + 1));
                MY_INT = ++local_value;
                try {
                    Thread.sleep(500);
                }
                catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

As stated in the article, the output you see consistently is:

Incrementing MY_INT to 1
Incrementing MY_INT to 2
Incrementing MY_INT to 3
Incrementing MY_INT to 4
Incrementing MY_INT to 5

Which is due to the fact that the ChangeListener is always reading MY_INT from its thread-local cache. (I tested this to be sure).

However, if you were to replace // (A) with any of the following:

  • Thread.currentThread().isAlive();
  • Thread.sleep(0)
  • Thread.interrupted()
  • System.out.print("");
  • adding a volatile integer x to ChangeListener and replacing // (A) with x += MY_INT

among other things, you would get the same output as you would by making MY_INT a volatile variable. That is,

Incrementing MY_INT to 1
Got Change for MY_INT : 1
Incrementing MY_INT to 2
Got Change for MY_INT : 2
Incrementing MY_INT to 3
Got Change for MY_INT : 3
Incrementing MY_INT to 4
Got Change for MY_INT : 4
Incrementing MY_INT to 5
Got Change for MY_INT : 5

What is is the common denominator with the operations I listed above that is causing the thread cache to clear?

EDIT: The last two suggestions clearly hit volatile or synchronized code leading to the "cache reset", but what about the others?

  • If `Thread.sleep()` has any memory happens-before semantics, I can't find it. I think what they all have in common is "blind luck." (Use proper synchronization and don't rely on weird stuff you see published online.) – markspace Dec 28 '22 at 17:08
  • @markspace how can you find the source code for `Thread.sleep()`? – Eitan Joseph Dec 28 '22 at 17:11
  • @markspace I'm just trying to understand how thread local cache works in Java and why these operations are resetting the cache when they dont appear to have synchronized blocks (aside from system out print and the volatile integer). – Eitan Joseph Dec 28 '22 at 17:13
  • I'm not convinced that they do reset any cache. If they do, it's accidental (implementation dependent) and you shouldn't use it in real code. It could change at any time. – markspace Dec 28 '22 at 17:17
  • 1
    There is no cache resetting. Caches on modern cpus are always coherent no matter if a variable volatile or not. Memory is just a spill bucket for whatever doesn’t fit in the cache. Volatile or synchronized are needed to ensure there is no data race. If there is a data race you can run into problems like visibility, reordering and atomicity of loads/stores. – pveentjer Dec 28 '22 at 18:43
  • Apart from having a data race, there is also a race condition because the increment isn’t atomic. – pveentjer Dec 28 '22 at 18:47
  • @pveentjer The increment is on the local variable. – Sotirios Delimanolis Dec 28 '22 at 21:03
  • There is no such thing as "thread-local cache" in Java. Modern CPU cores [usually have independent first-level and second-level caches](https://stackoverflow.com/a/954069/1643723), and support automatic cache-line synchronization across cores even without explicit synchronization instruction. But internal details of CPU implementations are orthogonal to Java. Java memory model operates in terms of "happens-before" semantics, which influence how JIT compiler performs inlining and issues memory barrier instructions. – user1643723 Jan 02 '23 at 10:18
  • If there is no reason to assume that variable will be modified from other threads, the JIT compiler might not even emit assembly code for reading from it. This situation — optimizing away the very existence of variable — is distinct from dealing with CPU cache lines. Cache lines are flushed by "memory barrier", while compiler behavior is kept in check by "compiler barrier". For example, native method can act as compiler barrier, because JIT does not know, what happens in native code of dll libraries. Memory barrier always implies compiler barrier, but not vice-versa. – user1643723 Jan 02 '23 at 10:29
  • Memory is flushed in cache lines, but flushing one cache line may have unexpected effects on other cache lines. C/C++ call this behavior ["memory order"](https://en.cppreference.com/w/cpp/atomic/memory_order). Java "happens-before" relationships usually refer to sequentially-consistent ordering: each volatile read/write affect all reads/writes before and after it. However Java Memory Model is not the same as C/C++ memory model (just like meaning of Java `volatile` is different from C/C++ `volatile` keyword). See [this answer](https://stackoverflow.com/a/7367168/1643723) for some insights. – user1643723 Jan 02 '23 at 10:49

1 Answers1

-2

I guess...It's a race condition. It means when 2 threads try to access some variable it caches it to the following few operations, to make it faster to operate. To avoid that, you can use AtomicInteger class it's thread-safe when you retrieve the integer it will be based on the current value in memory. Check https://www.tutorialspoint.com/java_concurrency/concurrency_atomic_integer.htm