让我们保存这段代码,它通过线程显示陈旧的缓存读取,从而阻止线程退出自己的while循环。
class MyRunnable implements Runnable {
boolean keepGoing = true; // volatile fixes visibility
@Override public void run() {
while ( keepGoing ) {
// synchronized (this) { } // fixes visibility
// Thread.yield(); // fixes visibility
System.out.println(); // fixes visibility
}
}
}
class Example {
public static void main(String[] args) throws InterruptedException{
MyRunnable myRunnable = new MyRunnable();
new Thread(myRunnable).start();
Thread.sleep(100);
myRunnable.keepGoing = false;
}
}我相信Java内存模型保证所有写入易失性变量的操作都与任何线程的所有后续读取同步,从而解决了这个问题。
如果我的理解是正确的,那么由同步块生成的代码也会清除所有挂起的读和写,这是一种“内存屏障”,并解决了这个问题。
在实践中,我看到插入yield和println也使变量更改对线程可见,并且它正确地退出。我的问题是:
产量/println/io是JMM在某种程度上保证的内存屏障,还是无法保证起作用的幸运副作用?
编辑:至少我在这个问题的措辞中所作的一些假设是错误的,例如关于同步块的假设。我鼓励问题的读者阅读下面的答案中的更正。
发布于 2021-09-15 14:17:49
规格中没有任何东西能保证任何形式的冲洗。这只是错误的心理模型,假设必须有某种东西,如主存,维持一个全球状态。但是,执行环境可以在每个CPU上都有本地内存,而根本没有主内存。因此CPU 1将更新的数据发送到CPU 2并不意味着CPU 3知道它。
实际上,系统有一个主内存,但是缓存可能是同步的,而不需要将数据传输到主内存。
此外,讨论内存传输的结果是隧道式的视觉。Java的内存模型也规定了JVM可能执行的优化和不执行的优化。例如。
nonVolatileVar = null;
Thread.sleep(100_000);
if(nonVolatileVar == null) {
// do something
}在这里,编译器有权删除条件并无条件地执行块,因为前面的语句(忽略睡眠)已经写入了null,而其他线程的活动与非volatile变量无关,不管经过多长时间。
因此,在执行此优化时,有多少线程向该变量写入一个新值并“刷新到内存”并不重要。这段代码不会注意到。
所以让我们咨询一下规格
需要注意的是,
Thread.sleep和Thread.yield都没有任何同步语义。特别是,编译器不必在调用Thread.sleep或Thread.yield之前将缓存在寄存器中的写操作刷新到共享内存,编译器也不必在调用Thread.sleep或Thread.yield之后重新加载缓存在寄存器中的值。
我想,你问题的答案再清楚不过了。
为了完整性
我相信Java内存模型保证所有写入易失性变量的操作都与任何线程的所有后续读取同步,从而解决了这个问题。
在写入volatile变量之前所做的所有写入都将对随后读取相同变量的线程可见。因此,在您的示例中,将keepGoing声明为volatile将修复这个问题,因为两个线程都一直在使用它。
如果我的理解是正确的,那么由同步块生成的代码也会清除所有挂起的读和写,这是一种“内存屏障”,并解决了这个问题。
离开synchronized块的线程建立了与使用同一个对象进入synchronized块的线程之间的关系。如果在一个线程中使用synchronized块似乎解决了这个问题,尽管在另一个线程中没有使用synchronized块,那么您依赖的是某个特定实现的副作用,该实现不能保证继续工作。
发布于 2021-09-14 05:53:53
让我们保存这段代码,它通过线程显示陈旧的缓存读取,从而阻止线程退出自己的while循环。
如果您指的是CPU缓存,那么这是一个糟糕的心智模型(除了不适合JMM的心智模型)。现代CPU上的缓存始终是一致的。
我相信Java内存模型保证所有写入易失性变量的操作都与任何线程的所有后续读取同步,从而解决了这个问题。
这是正确的。在一个易失性变量的写入和相同的易失性变量的所有后续读取之间有一个在边缘之前发生的情况。
如果我的理解是正确的话,由同步块生成的代码也会清除所有挂起的读写,这是一种“内存屏障”,并解决了这个问题。
从记忆障碍和JMM的结合来看,理性是危险的。
https://shipilev.net/blog/2016/close-encounters-of-jmm-kind/#myth-barriers-are-sane
在发布监视器和随后获取同一监视器之间的边界之前会发生这样的情况。因此,如果要在keepGoing变量受锁保护时访问它,则不存在数据竞争。
产量/println/io是JMM在某种程度上保护的内存屏障,还是无法保证起作用的幸运副作用?
检查JLS,你会发现在两个收益率之间的边缘之前没有发生。可能存在CPU内存障碍,但问题可能发生在代码到达CPU之前。例如,JIT可以优化代码,以便:
if(!keepGoing){
return;
}
while(true){
Thread.yield();
println();
}因此,在这种情况下,代码在CPU上执行之前已经“中断”了,因为代码将永远不会看到“keepGoing”变量的更新版本。
我不确定Thread.yield()是否有任何编译器障碍,是否存在编译器屏障,而JIT无法优化加载或存储。但这些都不是规范的一部分。
https://stackoverflow.com/questions/69169305
复制相似问题