[英]Memory effects of synchronization in Java
但是,除了相互排斥之外,还有更多的同步。 同步确保线程在同步块之前或期间的内存写入以可预测的方式显示给在同一监视器上同步的其他线程。 在我们退出synchronized块之后,我们释放了监视器,它具有将缓存刷新到主内存的效果,因此该线程所做的写操作对其他线程是可见的。 在我们进入同步块之前,我们获取监视器,它具有使本地处理器高速缓存无效的效果,以便从主存储器重新加载变量。 然后,我们将能够看到前一版本中显示的所有写入。
我还记得在现代Sun VM上阅读,无竞争同步很便宜。 这个说法我有点困惑。 考虑以下代码:
class Foo {
int x = 1;
int y = 1;
..
synchronized (aLock) {
x = x + 1;
}
}
对x的更新需要同步,但是锁的获取是否也从缓存中清除了y的值? 我无法想象会出现这种情况,因为如果确实如此,锁定条带化等技术可能无济于事。 或者,JVM可以可靠地分析代码以确保使用相同的锁在另一个同步块中不修改y,因此在进入同步块时不会在缓存中转储y的值吗?
简短的回答是JSR-133的解释太过分了 。 这不是一个严重的问题,因为JSR-133是一个非规范性文档,它不是语言或JVM标准的一部分。 相反,它只是一个文档,它解释了一个足以实现内存模型的可能策略,但通常不是必需的 。 最重要的是,关于“缓存刷新”的评论基本上完全不存在,因为基本上零架构将通过执行任何类型的“缓存刷新”来实现Java存储器模型(并且许多架构甚至没有这样的指令)。
Java内存模型是根据可见性,原子性,发生在之前的关系等方式正式定义的,它准确地解释了哪些线程必须看到什么,在其他操作之前必须发生什么操作以及使用精确(数学)定义的其他关系模型。 未正式定义的行为可能是随机的,或者在某些硬件和JVM实现中在实践中定义良好 - 但当然你不应该依赖于此,因为它可能在将来发生变化,你永远无法确定除非你编写了JVM并且非常了解硬件语义,否则它首先是定义良好的。
因此,您引用的文本并未正式描述Java所保证的内容,而是描述了具有非常弱的内存排序和可见性保证的某些假设架构如何使用缓存刷新来满足Java内存模型要求。 缓存刷新,主存等等的任何实际讨论显然通常不适用于Java,因为抽象语言和内存模型规范中不存在这些概念。
在实践中,内存模型提供的保证比完全刷新要弱得多 - 每个原子,并发相关或锁定操作冲洗整个缓存都会非常昂贵 - 而这几乎从未在实践中完成。 相反,使用特殊的原子CPU操作,有时与内存屏障指令结合使用,这有助于确保内存可见性和排序。 因此,廉价的无竞争同步和“完全刷新缓存”之间的明显不一致性通过注意第一个为真而第二个不是 - Java内存模型不需要完全刷新(并且在实践中不发生刷新)来解决。
如果正式的记忆模型有点太难以消化(你不会孤单),你也可以通过看看Doug Lea的烹饪书深入研究这个主题,这本书实际上是在JSR-133 FAQ中链接的,但是从具体的硬件角度出发,因为它是针对编译器编写者的。 在那里,他们确切地讨论了特定操作所需的障碍,包括同步 - 并且在那里讨论的障碍可以很容易地映射到实际硬件。 很多实际的映射都在食谱中讨论过。
BeeOnRope是对的,你引用的文本更多地涉及典型的实现细节,而不是Java内存模型确实保证的内容。 实际上,当您在x上进行同步时,您可能经常会看到y实际上是从CPU缓存中清除的(同样,如果示例中的x是volatile变量,在这种情况下,不需要显式同步来触发效果)。 这是因为在大多数CPU上(注意这是硬件效应,而不是JMM描述的),缓存在称为缓存行的单元上工作,缓存行通常比机器字长(例如64字节宽)。 由于只有完整的行可以在缓存中加载或无效,因此很有可能x和y将落入同一行,并且刷新其中一行也会刷新另一行。
可以编写一个显示此效果的基准。 创建一个只有两个volatile int字段的类,让两个线程执行一些操作(例如,在一个长循环中递增),一个在一个字段上,一个在另一个上。 操作时间。 然后,在两个原始字段之间插入16个int字段并重复测试(16 * 4 = 64)。 请注意,数组只是一个引用,因此16个元素的数组不会起作用。 您可能会看到性能的显着提高,因为一个字段上的操作不会再影响另一个字段。 这是否适合您将取决于JVM实现和处理器体系结构。 我已经在Sun JVM和典型的x64笔记本电脑上看到了这种情况,性能差异是几倍。
对x的更新需要同步,但是锁的获取是否也从缓存中清除了y的值? 我无法想象会出现这种情况,因为如果确实如此,锁定条带化等技术可能无济于事。
我不确定,但我认为答案可能是“是”。 考虑一下:
class Foo {
int x = 1;
int y = 1;
..
void bar() {
synchronized (aLock) {
x = x + 1;
}
y = y + 1;
}
}
现在这段代码是不安全的,取决于程序的其余部分会发生什么。 但是,我认为内存模型意味着bar
看到的y
值不应该大于获取锁定时的“实际”值。 这意味着缓存必须对y
和x
无效。
JVM也可以可靠地分析代码,以确保使用相同的锁在另一个同步块中不修改y吗?
如果锁是this
,那么一旦所有类都被预加载,这种分析看起来像是全局优化是可行的。 (我不是说这很容易,或者值得......)
在更一般的情况下,证明给定锁仅与给定的“拥有”实例一起使用的问题可能是难以处理的。
我们是java开发人员,我们只知道虚拟机,而不是真正的机器!
让我推理正在发生的事情 - 但我必须说我不知道我在说什么。
假设线程A在带有高速缓存A的CPU A上运行,线程B在带有高速缓存B的CPU B上运行,
线程A读取y; CPU A从主内存中取y,并将值保存在缓存A中。
线程B为'y'分配新值。 VM此时不必更新主内存; 就线程B而言,它可以在'y'的本地图像上读/写; 也许'y'只不过是一个cpu寄存器。
线程B退出同步块并释放监视器。 (进入区块的时间和地点无关紧要)。 线程B已经更新了一些变量,直到这一点,包括'y'。 所有这些更新现在必须写入主内存。
CPU B将新y值写入主存储器中的'y'。 (我想这一点)几乎是立即,信息'主要更新'连接到缓存A,缓存A使自己的y副本无效。 这肯定发生在硬件上非常快。
线程A获取一个监视器并进入一个同步块 - 此时它不需要对缓存A做任何事情。'y'已经从缓存A中消失了。当线程A再次读取y时,它从主内存中刷新了由B指定的新值
考虑另一个变量z,它在步骤(1)中也被A缓存,但是在步骤(2)中它没有被线程B更新。 它可以在缓存A中一直存活到步骤(5)。 由于同步,对'z'的访问不会减慢。
如果上述陈述有意义,那么确实成本不是很高。
除了步骤(5)之外:线程A可以有自己的缓存,甚至比缓存A更快 - 例如,它可以使用寄存器来表示变量'y'。 这将不会被步骤(4)无效,因此在步骤(5)中,线程A必须在同步进入时擦除其自己的高速缓存。 但这并不是一个巨大的惩罚。
你可能想查看jdk6.0文档http://java.sun.com/javase/6/docs/api/java/util/concurrent/package-summary.html#MemoryVisibility
内存一致性属性Java语言规范的第17章定义了内存操作的发生前关系,例如共享变量的读写。 只有在读取操作之前发生写入操作时,一个线程的写入结果才能保证对另一个线程的读取可见。 synchronized和volatile构造以及Thread.start()和Thread.join()方法可以形成先发生关系。 特别是:
因此,正如上面突出显示的那样:在监视器上发生解锁之前发生的所有更改对于所有那些在同一监视器上锁定的线程(以及自己的同步块)都是可见的。这与Java的发生一致 - 语义之前。 因此,当某个其他线程获取“aLock”上的监视器时,对y所做的所有更改也将刷新到主内存。
同步保证,只有一个线程可以进入代码块。 但它并不能保证在同步部分内完成的变量修改对其他线程是可见的。 只有进入同步块的线程才能保证看到更改。 Java中同步的内存效应可以与针对c ++和Java的双重检查锁定问题进行比较。双重检查锁定被广泛引用并用作在多线程环境中实现延迟初始化的有效方法。 不幸的是, 当在Java中实现时 ,它将无法以独立于平台的方式可靠地工作 ,而无需额外的同步。 当用其他语言(如C ++)实现时,它取决于处理器的内存模型,编译器执行的重新排序以及编译器和同步库之间的交互。 由于这些都不是用C ++这样的语言指定的,因此对它的工作情况几乎没有什么可说的。 可以使用显式内存屏障使其在C ++中工作,但这些障碍在Java中不可用。
由于y超出了synchronized方法的范围,因此无法保证对其的更改在其他线程中可见。 如果要保证在所有线程中看到对y的更改是相同的,则所有线程在读/写y时必须使用同步。
如果某些线程以同步方式更改y而其他线程没有,那么您将获得意外行为。 必须同步线程之间共享的所有可变状态,以便在线程之间看到更改时有任何保证。 必须同步所有线程对共享可变状态(变量)的所有访问。
是的,JVM保证在保持锁定时没有其他线程可以进入受同一锁保护的代码区域。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.