Java的内存模型基于“发生在之前”关系,该关系强制执行规则,但也允许在缓存失效方面优化虚拟机的实现。
例如,在以下情况中:
// thread A
private void method() {
//code before lock
synchronized (lockA) {
//code inside
}
}
// thread B
private void method2() {
//code before lock
synchronized (lockA) {
//code inside
}
}
// thread B
private void method3() {
//code before lock
synchronized (lockB) {
//code inside
}
}
如果线程A调用method()
并且线程B尝试在lockA
内获取method2()
,则lockA
上的同步将要求线程B观察线程A的所有更改在释放锁之前对其所有变量进行了修改,甚至是“锁定前代码”部分中更改的变量。
另一方面,method3()
使用另一个锁,并且不会强制执行之前发生的事件。这为优化创造了机会。
我的问题是虚拟机如何实现这些复杂的语义?它是否避免在不需要时完全刷新缓存?
它如何跟踪哪个变量在哪个点被哪个线程更改,以便它只从内存中加载所需的缓存行?
答案 0 :(得分:4)
您希望对JVM的思考过于高级。内存模型只是故意描述必须保证的内容,而不是必须如何实现。某些体系结构具有连贯的缓存,根本不需要刷新。但是,当禁止重写读取和/或写入超出某一点时,可能需要采取措施。
但是在所有情况下,这些影响都是全局,因为对所有读取和写入都进行了保证,而不依赖于建立先发生关系的特定构造。回想一下,所有写入在发布特定锁定之前发生 - 在所有获取相同锁定之后读取。
JVM根本不处理发生在之前的关系。它通过解释(执行)代码或为其生成本机代码来处理代码。这样做时,它必须通过插入障碍或冲洗以及不重新排序超出这些障碍的读或写指令来服从存储器模型。此时,它通常会孤立地考虑代码,而不是查看其他线程正在做什么。这些冲刷或障碍的影响总是全球性的。
但是,具有全局效应不足以建立先发生过的关系。这种关系只存在,当一个线程保证在之前提交所有写入时,保证另一个线程(重新)读取值。当两个线程在不同对象上同步或获取/释放不同的锁时,此排序不存在。
对于volatile
变量,您可以评估变量的值以查找,其他线程是否已写入预期值并因此提交写入。如果是synchronized
块,则互斥会强制执行排序。因此,在synchronized
块中,线程可以检查监视器保护的所有变量以评估状态,这应该是使用同一监视器在synchronized
块内的先前更新的结果。
由于这些影响是全局性的,一些开发人员误以为只要关于时间排序的假设“合理”,就可以认为在不同的锁上进行同步是正常的,但是这样的程序代码必须被认为是破坏的,因为它依赖于特定实现的副作用,尤其是其简单性。
最近的JVM做的一件事是考虑纯粹本地的对象,即任何其他线程从未见过的对象,在同步它们时不能建立先发生关系。因此,在这些情况下可以省略同步的效果。我们预计未来会有更多的优化......
答案 1 :(得分:1)
它如何跟踪哪个变量在哪个点被哪个线程更改,以便它只从内存中加载所需的缓存行?
没有。这不是现代CPU的工作方式。
在您可能会看到运行多线程Java代码的每个平台上都存在足够复杂的问题,因此缓存一致性在硬件中实现。高速缓存行可以直接从一个高速缓存传输到另一个高速缓存而无需通过主存储器。实际上,如果每次将数据放在一个核心上并在另一个核心上拾取数据时,数据必须通过慢速主存储器,这将是非常糟糕的。所以缓存直接相互通信。
当代码修改内存地址时,该内核的缓存获取该内存地址的独占所有权。如果另一个核心想要读取该存储器地址,则高速缓存通常将通过直接通信共享存储器地址。如果任一核心想要修改共享数据,它必须使另一个线程的缓存中的数据无效。
因此,这些缓存由硬件管理,有效地使它们在软件级别不可见。
但是,CPU确实有时会预取或发布写入(不在缓存中)。这些只需要使用内存屏障指令。内存屏障完全在CPU内部运行,以防止跨越屏障的内存操作的重新排序,延迟或提前执行。 CPU知道哪些内存操作被延迟或提前执行,因此代码不必跟踪它。