简体   繁体   中英

Broken synchronization without volatile variable

I would like to understand what happens from a cache point-of-view (MESI protocol) if a programmer forget to add the volatile keyword to a variable used for synchronization.

The following snippet of code simply stops after some iterations. I know it's because one of the 2 threads (let's assume THREAD 0) does not see the update made to current variable by THREAD 1 done by getNext(), therefore it keeps looping forever.

However, I don't understand why it is the case. THREAD 0 is looping on current and should see at some point that the cache line was updated by THREAD 1 (the cache line switched to Modified state) and issue a "Read" message on the memory bus to fetch it in its local cache. Adding volatile modifier to current variable will make everything works fine.

What is happening that prevent THREAD 0 to continue its execution ?

Reference: Memory Barriers: a Hardware View for Software Hackers

public class Volatile {
    public static final int THREAD_0 = 0;
    public static final int THREAD_1 = 1;
    public static int current;
    public static int counter = 0;

    public static void main(String[] args) {
        current = 0;

        /** Thread 0 **/
        Thread thread0 = new Thread(() -> {
            while(true) { /** THREAD_0 */
                while (current != THREAD_0);

                counter++;
                System.out.println("Thread0:" + counter);

                current = getNext(THREAD_0);
            }
        });

        /** Thread 1 **/
        Thread thread1 = new Thread(() -> {
            while(true) { /** THREAD_1 */
                while (current != THREAD_1);

                counter++;
                System.out.println("Thread1:" + counter);

                current = getNext(THREAD_1);
            }
        });

        thread0.start();
        thread1.start();
    }

    public static int getNext(int threadId) {
        return threadId == THREAD_0 ? THREAD_1 : THREAD_0;
    }
}

Different processor architectures can have different degrees of cache coherence. Some may be pretty minimal, because the goal is to maximize throughput and synching up caches with memory unnecessarily will drag that down. Java imposes its own memory barriers when it detects that coordination is required, but that depends on the programmer following the rules.

If the programmer doesn't add indications (like using the synchronized or volatile keywords) that a variable can be shared across threads, the JIT is allowed to assume that no other thread is modifying that variable, and it can optimize accordingly. The bytecode that tests that variable can get reordered drastically. If the JIT reorders the code enough then when the hardware detects a modification and fetches the new value may not matter.

Quoting from Java Concurrency in Practice 3.1:

In the absence of synchronization, the compiler, processor, and runtime can do some downright weird things to the order in which operations appear to execute. Attempts to reason about the order in which memory actions "must" happen in insufficiently synchronized multithreaded programs will almost certainly be incorrect.

(There are several places in the JCIP book making the case that reasoning about insufficiently-synchronized code is futile.)

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM