java 内存模型是否可以防止 volatile 变量之间来自不同线程的交错写入?

Does the java memory model prevent interleaving writes from different threads between volatile variables?

我正在阅读一篇关于 the memory barriers and their role in JVM concurrency 的有趣文章,Dekker 算法的示例实现引起了我的注意

     volatile boolean intentFirst = false;
     volatile boolean intentSecond = false;
     volatile int turn = 0;


     // code run by first thread    // code run by second thread

 1    intentFirst = true;           intentSecond = true;
 2
 3    while (intentSecond) {        while (intentFirst) {     // volatile read
 4      if (turn != 0) {               if (turn != 1) {       // volatile read
 5        intentFirst = false;               intentSecond = false;
 6        while (turn != 0) {}               while (turn != 1) {}
 7        intentFirst = true;                intentSecond = true;
 8      }                              }
 9    }                             }
10    criticalSection();            criticalSection();
11
12    turn = 1;                     turn = 0;                 // volatile write
13    intentFirst = false;          intentSecond = false;     // volatile write

文章中提到,由于volatiles是顺序一致的,临界区必然由一个线程执行,通过happens-before保证check out。但是,如果两个线程继续循环执行相同的逻辑,这是否仍然成立?我的低估是,底层 OS 调度程序可能会决定在第 7 行执行之前暂停后续执行中的第二个线程,并让第一个线程命中临界区,同时, OS恢复第二个线程并同时命中临界区。我的理解是否正确,给出这个例子的想法是这段代码只执行一次?如果是这样,我假设我的问题的答案是“否”,因为易失性仅用于保证内存可见性。

编译器确保平台上的编译代码(在本例中为 JVM)在 reordering of statements(在许多级别上)和线程之间的可见性方面为易失性变量提供某些保证。在大多数具有 JVM 实现的平台上,这可以在不涉及操作系统的情况下完成,并且不涉及调度。

关于重新排序和内存屏障的保证本身没有排除机制,需要进入临界区。这些可以通过许多不同的方式实现,例如通过 Dekker 算法。 Dekker 的算法是所谓的 busy 算法,只要不允许进入临界区,它就需要 CPU 工作。在现代硬件上,更简单的算法是可能的,可以使用 CAS 操作,例如

// declare an atomic boolean (package java.util.concurrent.atomic)
AtomicBoolean busyFlag = new AtomicBoolean(false);

// the part with the critical section, same for all threads
while (!busyFlag.compareAndSet(false, true)) { /* busy wait */ }
try {
    criticalSection();
}
finally {
    busyFlag.set(false);
}

在这种情况下,[compareAndSet][3] 确保标志自动设置 为真当且仅当它为假时,returns 为真发生了。

但是,就像 Dekker 算法一样,这仍然是一个繁忙的机制,如果您运气不好,可能会花费您很多 CPU。 synchronized段或者Lock段会在无法获取锁的时候要求操作系统挂起当前线程。但这与 volatile.

无关

My understating is that the underlying OS scheduler may decide to pause the second thread in subsequent execution just before line 7 is executed and leave the first thread hit the critical section, and at the same moment, the OS resume the second thread and hit the critical section simultaneously.

这不会发生。如果第二个线程被挂起并且第一个线程 在临界区中,intentFirst 仍然为真,因为它仅在第一个线程离开临界区后才设置为假。所以如果第二个线程被唤醒,intendSecond设置为真,但是第二个线程会卡在while(intendedFirst)循环中,延迟进入临界区,直到第一个线程兴奋。

恕我直言,关于 happens-before 的 Dekkers 算法最有趣的部分在这里:

intentSecond=true; // volatile write
while(intendFirst) // volatile read

现代处理器有存储缓冲区,这可能会导致旧存储被重新排序为不同地址的新负载。这就是为什么在前面的存储和后面的加载之间需要一个[StoreLoad]来确保它们顺序一致。