java Java中同步的记忆效应
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/1850270/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
Memory effects of synchronization in Java
提问by Binil Thomas
JSR-133 FAQsays:
But there is more to synchronization than mutual exclusion. Synchronization ensures that memory writes by a thread before or during a synchronized block are made visible in a predictable manner to other threads which synchronize on the same monitor. After we exit a synchronized block, we release the monitor, which has the effect of flushing the cache to main memory, so that writes made by this thread can be visible to other threads. Before we can enter a synchronized block, we acquire the monitor, which has the effect of invalidating the local processor cache so that variables will be reloaded from main memory. We will then be able to see all of the writes made visible by the previous release.
但是同步不仅仅是互斥。同步确保在同步块之前或期间线程写入的内存以可预测的方式对在同一监视器上同步的其他线程可见。在我们退出一个同步块后,我们释放监视器,它具有将缓存刷新到主内存的效果,以便其他线程可以看到该线程所做的写入。在我们可以进入同步块之前,我们获取监视器,它具有使本地处理器缓存失效的效果,以便变量将从主内存中重新加载。然后,我们将能够看到上一版本可见的所有写入。
I also remember reading that on modern Sun VMs uncontended synchronizations are cheap. I am a little confused by this claim. Consider code like:
我还记得读到在现代 Sun VM 上无竞争的同步很便宜。我对这个说法有点困惑。考虑如下代码:
class Foo {
int x = 1;
int y = 1;
..
synchronized (aLock) {
x = x + 1;
}
}
Updates to x need the synchronization, but does the acquisition of the lock clear the value of y also from the cache? I can't imagine that to be the case, because if it were true, techniques like lock striping might not help. Alternatively can the JVM reliably analyze the code to ensure that y is not modified in another synchronized block using the same lock and hence not dump the value of y in cache when entering the synchronized block?
更新 x 需要同步,但是获取锁是否也从缓存中清除了 y 的值?我无法想象会是这样,因为如果这是真的,像锁条带这样的技术可能无济于事。或者,JVM 能否可靠地分析代码以确保 y 不会在另一个使用相同锁的同步块中被修改,从而在进入同步块时不会在缓存中转储 y 的值?
采纳答案by BeeOnRope
The short answer is that JSR-133 goes too far in its explanation. This isn't a serious issue because JSR-133 is a non-normative document which isn't part of the language or JVM standards. Rather, it is only a document which explains one possible strategy that is sufficientfor implementing the memory model, but isn't in general necessary. On top of that, the comment about "cache flushing" is basically totally out place since essentially zero architectures would implement the Java memory model by doing any type of "cache flushing" (and many architectures don't even have such instructions).
简短的回答是JSR-133 在它的解释中走得太远了。这不是一个严重的问题,因为 JSR-133 是一个非规范性文档,它不是语言或 JVM 标准的一部分。相反,它只是一个文档,解释了一种足以实现内存模型的可能策略,但通常不是必需的。最重要的是,关于“缓存刷新”的评论基本上完全不合适,因为基本上零架构将通过执行任何类型的“缓存刷新”来实现 Java 内存模型(许多架构甚至没有这样的指令)。
The Java memory model is formally defined in terms of things like visibility, atomicity, happens-before relationships and so on, which explains exactly what threads mustsee what, what actions mustoccur before other actions and other relationships using a precisely (mathematically) defined model. Behavior which isn't formally defined could be random, or well-defined in practice on some hardware and JVM implementation - but of course you should never rely on this, as it might change in the future, and you could never really be sure that it was well-defined in the first place unless you wrote the JVM and were well-aware of the hardware semantics.
Java 内存模型根据可见性、原子性、发生在关系等方面进行了正式定义,它使用精确(数学上)定义的方式准确地解释了哪些线程必须查看哪些内容、哪些操作必须在其他操作之前发生以及其他关系模型。未正式定义的行为可能是随机的,也可能是在某些硬件和 JVM 实现中在实践中明确定义的 - 但当然你不应该依赖它,因为它可能在未来发生变化,而且你永远无法真正确定除非您编写了 JVM 并且非常了解硬件语义,否则它首先是明确定义的。
So the text that you quoted is not formally describing what Java guarantees, but rather is describing how some hypothetical architecture which had very weak memory ordering and visibility guarantees couldsatisfy the Java memory model requirements using cache flushing. Any actual discussion of cache flushing, main memory and so on is clearly not generally applicable to Java as these concepts don't exist in the abstract language and memory model spec.
因此,您引用的文本并未正式描述 Java 保证的内容,而是描述了某些具有非常弱的内存排序和可见性保证的假设架构如何使用缓存刷新来满足 Java 内存模型要求。任何关于缓存刷新、主内存等的实际讨论显然都不适用于 Java,因为这些概念在抽象语言和内存模型规范中不存在。
In practice, the guarantees offered by the memory model are much weaker than a full flush - having every atomic, concurrency-related or lock operation flush the entire cache would be prohibitively expensive - and this is almost never done in practice. Rather, special atomic CPU operations are used, sometimes in combination with memory barrierinstructions, which help ensure memory visibility and ordering. So the apparent inconsistency between cheap uncontended synchronization and "fully flushing the cache" is resolved by noting that the first is true and the second is not - no full flush is required by the Java memory model (and no flush occurs in practice).
在实践中,内存模型提供的保证比完全刷新要弱得多——让每个原子的、与并发相关的或锁定操作刷新整个缓存的成本高得令人望而却步——这在实践中几乎从未做过。相反,使用特殊的原子 CPU 操作,有时与内存屏障指令结合使用,这有助于确保内存可见性和排序。因此,通过注意到第一个是真的而第二个不是 - Java 内存模型不需要完全刷新(并且在实践中没有发生刷新),从而解决了廉价的无竞争同步和“完全刷新缓存”之间的明显不一致。
If the formal memory model is a bit too heavy to digest (you wouldn't be alone), you can also dive deeper into this topic by taking a look at Doug Lea's cookbook, which is in fact linked in the JSR-133 FAQ, but comes at the issue from a concrete hardware perspective, since it is intended for compiler writers. There, they talk about exactly what barriers are needed for particular operations, including synchronization - and the barriers discussed there can pretty easily be mapped to actual hardware. Much of the actual mapping is discussed right in the cookbook.
如果正式的内存模型有点难以消化(您不会孤单),您还可以通过查看Doug Lea 的食谱来更深入地研究这个主题,这实际上链接在 JSR-133 常见问题解答中,但是从具体的硬件角度来看这个问题,因为它是为编译器编写者准备的。在那里,他们确切地讨论了特定操作所需的障碍,包括同步——那里讨论的障碍可以很容易地映射到实际硬件。大部分实际映射都在说明书中进行了讨论。
回答by Micha? Kosmulski
BeeOnRope is right, the text you quote delves more into typical implementation details than into what the Java Memory Model does indeed guarantee. In practice, you may often see that y is actually purged from CPU caches when you synchronize on x (also, if x in your example were a volatile variable in which case explicit synchronization is not necessary to trigger the effect). This is because on most CPUs (note that this is a hardware effect, not something the JMM describes), the cache works on units called cache lines, which are usually longer than a machine word (for example 64 bytes wide). Since only complete lines can be loaded or invalidated in the cache, there are good chances that x and y will fall into the same line and that flushing one of them will also flush the other one.
BeeOnRope 是对的,您引用的文本更多地探讨了典型的实现细节,而不是 Java 内存模型确实保证的内容。在实践中,您可能经常看到当您在 x 上同步时 y 实际上从 CPU 缓存中清除了(此外,如果您的示例中的 x 是一个易失性变量,在这种情况下,不需要显式同步来触发效果)。这是因为在大多数 CPU 上(请注意,这是硬件效应,而不是 JMM 所描述的),缓存在称为缓存行的单元上工作,这些单元通常比机器字长(例如 64 字节宽)。由于只有完整的行可以在缓存中加载或失效,因此 x 和 y 很有可能会落入同一行,并且刷新其中一个也会刷新另一个。
It is possible to write a benchmark which shows this effect. Make a class with just two volatile int fields and let two threads perform some operations (e.g. incrementing in a long loop), one on one of the fields and one on the another. Time the operation. Then, insert 16 int fields in between the two original fields and repeat the test (16*4=64). Note that an array is just a reference so an array of 16 elements won't do the trick. You may see a significant improvement in performance because operations on one field will not influence the other one any more. Whether this works for you will depend on the JVM implementation and processor architecture. I have seen this in practice on Sun JVM and a typical x64 laptop, the difference in performance was by a factor of several times.
可以编写一个基准测试来显示这种效果。创建一个只有两个 volatile int 字段的类,让两个线程执行一些操作(例如在长循环中递增),一个在一个字段上,一个在另一个字段上。操作时间。然后,在两个原始字段之间插入 16 个 int 字段并重复测试(16*4=64)。请注意,数组只是一个引用,因此 16 个元素的数组不会起作用。您可能会看到性能的显着提高,因为对一个字段的操作不会再影响另一个字段。这是否适合您将取决于 JVM 实现和处理器架构。我已经在 Sun JVM 和典型的 x64 笔记本电脑上实际看到了这一点,性能差异是数倍。
回答by Stephen C
Updates to x need the synchronization, but does the acquisition of the lock clear the value of y also from the cache? I can't imagine that to be the case, because if it were true, techniques like lock striping might not help.
更新 x 需要同步,但是获取锁是否也从缓存中清除了 y 的值?我无法想象会是这样,因为如果这是真的,像锁条带这样的技术可能无济于事。
I'm not sure, but I think the answer may be "yes". Consider this:
我不确定,但我认为答案可能是“是”。考虑一下:
class Foo {
int x = 1;
int y = 1;
..
void bar() {
synchronized (aLock) {
x = x + 1;
}
y = y + 1;
}
}
Now this code is unsafe, depending on what happens im the rest of the program. However, I think that the memory model means that the value of yseen by barshould not be older than the "real" value at the time of acquisition of the lock. That would imply the cache must be invalidated for yas well as x.
现在这段代码是不安全的,这取决于程序的其余部分发生了什么。但是,我认为内存模型意味着在获取锁时,yseen by的值bar不应比“真实”值更旧。这意味着缓存必须y为 以及无效x。
Also can the JVM reliably analyze the code to ensure that y is not modified in another synchronized block using the same lock?
JVM 能否可靠地分析代码以确保 y 不会在另一个使用相同锁的同步块中被修改?
If the lock is this, this analysis looks like it would be feasible as a global optimization once all classes have been preloaded. (I'm not saying that it would be easy, or worthwhile ...)
如果锁定是this,则一旦所有类都已预加载,此分析看起来作为全局优化是可行的。(我并不是说这很容易,或者值得......)
In more general cases, the problem of proving that a given lock is only ever used in connection with a given "owning" instance is probably intractable.
在更一般的情况下,证明给定的锁只与给定的“拥有”实例结合使用的问题可能是棘手的。
回答by irreputable
we are java developers, we only know virtual machines, not real machines!
我们是java开发者,只知道虚拟机,不知道真机!
let me theorize what is happening - but I must say I don't know what I'm talking about.
让我把正在发生的事情理论化——但我必须说我不知道我在说什么。
say thread A is running on CPU A with cache A, thread B is running on CPU B with cache B,
假设线程 A 运行在带有缓存 A 的 CPU A 上,线程 B 运行在带有缓存 B 的 CPU B 上,
thread A reads y; CPU A fetches y from main memory, and saved the value in cache A.
thread B assigns new value to 'y'. VM doesn't have to update the main memory at this point; as far as thread B is concerned, it can be reading/writing on a local image of 'y'; maybe the 'y' is nothing but a cpu register.
thread B exits a sync block and releases a monitor. (when and where it entered the block doesn't matter). thread B has updated quite some variables till this point, including 'y'. All those updates must be written to main memory now.
CPU B writes the new y value to place 'y' in main memory. (I imagine that) almost INSTANTLY, information 'main y is updated' is wired to cache A, and cache A invalidate its own copy of y. That must have happened really FAST on the hardware.
thread A acquires a monitor and enters a sync block - at this point it doesn't have to do anything regarding cache A. 'y' has already gone from cache A. when thread A reads y again, it's fresh from main memory with the new value assigned by B.
线程 A 读取 y;CPU A 从主存中取出 y,并将值保存在缓存 A 中。
线程 B 为“y”分配新值。此时 VM 不必更新主内存;就线程 B 而言,它可以读取/写入 'y' 的本地图像;也许'y'只是一个cpu寄存器。
线程 B 退出同步块并释放监视器。(它进入块的时间和地点无关紧要)。到目前为止,线程 B 已经更新了相当多的变量,包括“y”。现在所有这些更新都必须写入主内存。
CPU B 将新的 y 值写入主存储器中的“y”。(我想)几乎立即,信息“主要 y 已更新”连接到缓存 A,缓存 A 使其自己的 y 副本无效。这一定在硬件上发生得非常快。
线程 A 获得一个监视器并进入一个同步块——此时它不需要对缓存 A 做任何事情。“y”已经从缓存 A 中消失了。当线程 A 再次读取 y 时,它从主内存中刷新B 分配的新值。
consider another variable z, which was also cached by A in step(1), but it's not updated by thread B in step(2). it can survive in cache A all the way to step(5). access to 'z' is not slowed down because of synchronization.
考虑另一个变量 z,它也在步骤 (1) 中被 A 缓存,但它没有在步骤 (2) 中被线程 B 更新。它可以在缓存 A 中一直存在到第 (5) 步。对“z”的访问不会因为同步而变慢。
if the above statements make sense, then indeed the cost isn't very high.
如果上面的说法有道理,那么成本确实不是很高。
addition to step(5): thread A may have its own cache which is even faster than cache A - it can use a register for variable 'y' for example. that will not be invalidated by step(4), therefore in step(5), thread A must erase its own cache upon sync entering. that's not a huge penalty though.
除了步骤(5):线程 A 可能有自己的缓存,它比缓存 A 更快——例如,它可以使用变量“y”的寄存器。这不会被 step(4) 失效,因此在 step(5) 中,线程 A 必须在同步进入时擦除自己的缓存。虽然这不是一个巨大的惩罚。
回答by rohit kochar
you might want to check jdk6.0 documentation http://java.sun.com/javase/6/docs/api/java/util/concurrent/package-summary.html#MemoryVisibility
您可能想查看 jdk6.0 文档 http://java.sun.com/javase/6/docs/api/java/util/concurrent/package-summary.html#MemoryVisibility
Memory Consistency Properties Chapter 17 of the Java Language Specification defines the happens-before relation on memory operations such as reads and writes of shared variables. The results of a write by one thread are guaranteed to be visible to a read by another thread only if the write operation happens-before the read operation. The synchronized and volatile constructs, as well as the Thread.start() and Thread.join() methods, can form happens-before relationships. In particular:
内存一致性属性 Java 语言规范的第 17 章定义了内存操作(例如共享变量的读取和写入)的happens-before 关系。只有在写操作发生在读操作之前,才能保证一个线程的写入结果对另一个线程的读取可见。synchronized 和 volatile 构造,以及 Thread.start() 和 Thread.join() 方法,可以形成先发生关系。特别是:
- Each action in a thread happens-before every action in that thread that comes later in the program's order.
- An unlock (synchronized block or method exit) of a monitor happens-before every subsequent lock (synchronized block or method entry) of that same monitor. And because the happens-before relation is transitive, all actions of a thread prior to unlocking happen-before all actions subsequent to any thread locking that monitor.
- A write to a volatile field happens-before every subsequent read of that same field. Writes and reads of volatile fields have similar memory consistency effects as entering and exiting monitors, but do not entail mutual exclusion locking.
- A call to start on a thread happens-before any action in the started thread.
- All actions in a thread happen-before any other thread successfully returns from a join on that thread
- 线程中的每个操作都发生在该线程中按程序顺序稍后出现的每个操作之前。
- 监视器的解锁(同步块或方法退出)发生在同一监视器的每个后续锁定(同步块或方法入口)之前。并且因为happens-before 关系是可传递的,解锁之前线程的所有操作都发生在任何线程锁定该监视器之后的所有操作之前。
- 对 volatile 字段的写入发生在每次后续读取同一字段之前。易失性字段的写入和读取与进入和退出监视器具有类似的内存一致性效果,但不需要互斥锁定。
- 对启动线程的调用发生在已启动线程中的任何操作之前。
- 线程中的所有操作都发生在任何其他线程从该线程上的连接成功返回之前
So,as stated in highlighted point above:All the changes that happens before a unlock happens on a monitor is visible to all those threads(and in there own synchronization block) which take lock on the same monitor.This is in accordance with Java's happens-before semantics. Therefore,all changes made to y would also be flushed to main memory when some other thread acquires the monitor on 'aLock'.
因此,如上面突出显示的点所述:在监视器上发生解锁之前发生的所有更改对于在同一监视器上锁定的所有线程(以及在那里自己的同步块中)都是可见的。这与 Java 的发生一致-在语义之前。因此,当某个其他线程在“aLock”上获取监视器时,对 y 所做的所有更改也将刷新到主内存中。
回答by Sudhakar Kalmari
synchronize guarantees, that only one thread can enter a block of code. But it doesn't guarantee, that variables modifications done within synchronized section will be visible to other threads. Only the threads that enters the synchronized block is guaranteed to see the changes. Memory effects of synchronization in Java could be compared with the problem of Double-Checked Locking with respect to c++ and Java Double-Checked Locking is widely cited and used as an efficient method for implementing lazy initialization in a multi-threaded environment. Unfortunately, it will not work reliably in a platform independent way when implemented in Java, without additional synchronization. When implemented in other languages, such as C++, it depends on the memory model of the processor, the re-orderings performed by the compiler and the interaction between the compiler and the synchronization library. Since none of these are specified in a language such as C++, little can be said about the situations in which it will work. Explicit memory barriers can be used to make it work in C++, but these barriers are not available in Java.
同步保证,只有一个线程可以进入一个代码块。但它并不能保证在同步部分中完成的变量修改对其他线程可见。只有进入同步块的线程才能保证看到更改。Java 中同步的内存效应可以与 C++ 中的双重检查锁定问题进行比较,Java 双重检查锁定被广泛引用并用作在多线程环境中实现延迟初始化的有效方法。不幸的是,当用 Java 实现时,它不会以独立于平台的方式可靠地工作,无需额外同步。当用其他语言(例如 C++)实现时,它取决于处理器的内存模型、编译器执行的重新排序以及编译器与同步库之间的交互。由于这些都不是在诸如 C++ 之类的语言中指定的,因此几乎无法说明它将在哪些情况下工作。显式内存屏障可用于使其在 C++ 中工作,但这些屏障在 Java 中不可用。
回答by Jeremy Raymond
Since y is out of scope of the synchronized method there are no guarantees that changes to it are visible in other threads. If you want to guarantee that changes to y are seen the same across all threads, then allthreads must use synchronization when reading/writing y.
由于 y 超出了 synchronized 方法的范围,因此不能保证对它的更改在其他线程中可见。如果您想保证所有线程都可以看到对 y 的更改,那么所有线程在读/写 y 时必须使用同步。
If some threads change y in a synchronized manner and others don't then you'll get unexpected behavior. All mutable state shared between threads must be synchronized for there to be any guarantees on seeing changes between threads. All access on all threads to shared mutable state (variables) must be synchronized.
如果某些线程以同步方式更改 y 而其他线程不更改,那么您将获得意外行为。线程之间共享的所有可变状态都必须同步,才能保证看到线程之间的变化。所有线程对共享可变状态(变量)的所有访问都必须同步。
And yes, the JVM guarantees that while the lock is held no other thread can enter a region of code protected by the same lock.
是的,JVM 保证在持有锁时没有其他线程可以进入受同一锁保护的代码区域。

