17

Java内存模型有一些让我烦恼的东西(如果我什至正确理解了所有内容)。如果有两个线程 A 和 B,则不能保证 B 会看到 A 写入的值,除非 A 和 B 在同一个监视器上同步。

对于任何保证线程间缓存一致性的系统架构,都没有问题。但是如果架构在硬件上不支持缓存一致性,这实质上意味着每当一个线程进入一个监视器,之前所做的所有内存更改都必须提交到主内存,并且缓存必须失效。它必须是整个数据缓存,而不仅仅是几行,因为监视器没有信息它保护内存中的哪些变量。但这肯定会影响任何需要频繁同步的应用程序的性能(尤其是诸如具有短运行作业的作业队列之类的东西)。那么,Java 能否在没有硬件缓存一致性的架构上运行得相当好呢?如果不是,为什么内存模型不对可见性做出更强有力的保证?如果该语言需要监视器所保护的信息,那不是更有效吗?

正如我所看到的,内存模型为我们提供了两全其美的条件,即绝对需要同步,即使在硬件中保证了缓存一致性,另一方面,在不连贯的架构上性能不佳(完全缓存刷新)。那么它不应该更严格(需要由监视器保护的信息)还是更多地丢失并将潜在平台限制为缓存一致的架构?

就目前而言,这对我来说没有太大意义。有人可以弄清楚为什么选择这种特定的内存模型吗?


编辑:回想起来,我使用严格和失败是一个糟糕的选择。我用“严格”表示保证较少的情况,而“失败”表示相反的情况。为了避免混淆,最好用更强或更弱的保证来说话。

4

4 回答 4

6

绝对需要同步,即使在硬件中保证缓存一致性

是的,但是您只需要针对 Java 内存模型进行推理,而不是针对您的程序恰好在其上运行的特定硬件架构。另外,这不仅与硬件有关,编译器和 JIT 本身可能会重新排序导致可见性问题的指令。Java 中的同步结构在所有可能的代码转换级别(例如编译器/JIT/CPU/缓存)上始终如一地解决可见性和原子性问题。

另一方面,在不连贯的架构上表现不佳(完全缓存刷新)

也许我误解了 s/t,但是对于不连贯的架构,无论如何你都必须同步关键部分。否则,由于重新排序,您将遇到各种竞争条件。我不明白为什么 Java 内存模型会使事情变得更糟。

不应该更严格吗(需要监视器所保护的信息)

我认为根本不可能告诉 CPU 刷新缓存的任何特定部分。编译器能做的最好的事情是发出内存栅栏,让 CPU 决定缓存的哪些部分需要刷新——它仍然比我想的更粗粒度。即使可以进行更细粒度的控制,我认为这会使并发编程更加困难(已经够困难了)。

AFAIK,Java 5 MM(就像 .NET CLR MM)比 x86 和 IA64 等常见架构的内存模型更“严格”。因此,它使关于它的推理相对简单。然而,它显然不应该提供更接近顺序一致性的 s/t,因为这会显着损害性能,因为可以应用更少的编译器/JIT/CPU/缓存优化。

于 2010-06-16T15:06:39.277 回答
5

现有的架构保证缓存的一致性,但它们不保证顺序的一致性——这两件事是不同的。自序。不保证一致性,硬件允许某些重新排序,您需要关键部分来限制它们。关键部分确保一个线程写入的内容对另一个线程可见(即,它们防止数据竞争),并且它们还防止经典的竞争条件(如果两个线程增加相同的变量,您需要为每个线程读取当前值和新值的写入是不可分割的)。

此外,执行模型并不像您描述的那么昂贵。在大多数现有架构上,它们是缓存一致但不是顺序一致的,当您释放锁时,您必须将挂起的写入刷新到内存,并且当您获得一个时,您可能需要做一些事情来确保未来的读取不会读取过时的值 -大多数情况下,这意味着只是防止读取移动得太早,因为缓存保持一致;但仍然不能移动读取。

最后,您似乎认为 Java 的内存模型 (JMM) 很奇特,而其基础现在相当先进,类似于 Ada、POSIX 锁(取决于标准的解释)和 C /C++ 内存模型。您可能想阅读 JSR-133 食谱,它解释了如何在现有架构上实现 JMM:http: //g.oswego.edu/dl/jmm/cookbook.html

于 2011-08-16T02:25:21.757 回答
4

答案是大多数多处理器是缓存一致的,包括大型 NUMA 系统,这几乎是什么?总是 ccNUMA。

我认为您对在实践中如何实现缓存一致性有些困惑。首先,缓存可能与系统上的其他几个事物一致/不一致:

  • 设备
  • (内存修改者)DMA
  • 数据缓存与指令缓存
  • 其他核心/处理器上的缓存(这个问题是关于)
  • ...

必须采取一些措施来保持一致性。当使用设备和 DMA 时,在缓存与 DMA/设备不一致的架构上,您将绕过缓存(可能还有写缓冲区),或者在涉及 DMA/设备的操作周围使缓存无效/刷新。

同样,在动态生成代码时,您可能需要刷新指令缓存。

当涉及到 CPU 缓存时,一致性是使用一些一致性协议来实现的,例如 MESI、MOESI ......这些协议定义了在缓存之间发送的消息以响应某些事件(例如:当非-exclusive cacheline 被修改,...)。

虽然这足以维持(最终)一致性,但它不能保证顺序,或者其他 CPU 可以立即看到更改。然后,还有写入缓冲区,它们延迟写入。

因此,每个 CPU 架构都提供排序保证(例如,在对齐存储之前的访问不能在存储之后重新排序)和/或提供指令(内存屏障/栅栏)来请求此类保证。最后,进入/退出监视器并不需要刷新缓存,但可能需要耗尽写入缓冲区和/或等待读取结束。

于 2011-06-14T21:34:50.597 回答
1

JVM 可以访问的缓存实际上只是 CPU 寄存器。由于它们并不多,因此在监视器退出时冲洗它们并不是什么大问题。

编辑:(通常)内存缓存不受 JVM 控制,JVM 不能选择读/写/刷新这些缓存,所以在这个讨论中忘记它们

假设每个 CPU 有 1,000,000 个寄存器。JVM 愉快地利用它们进行疯狂的快速计算——直到它遇到监视器进入/退出,并且必须将 1,000,000 个寄存器刷新到下一个缓存层。

如果我们生活在那个世界中,Java 必须足够聪明地分析哪些对象不共享(大多数对象不共享),或者它必须要求程序员这样做。

java内存模型是一种简化的编程模型,可以让普通程序员做出OK的多线程算法。我所说的“简化”是指全世界可能有 12 个人真正阅读了 JLS 的第 17 章并真正理解了它。

于 2010-06-16T16:56:52.180 回答