46

JSR-133 常见问题解答说:

但同步不仅仅是互斥。同步确保线程在同步块之前或期间写入的内存以可预测的方式对在同一监视器上同步的其他线程可见。退出同步块后,我们释放监视器,这具有将缓存刷新到主内存的效果,以便该线程进行的写入可以对其他线程可见。在我们进入同步块之前,我们获取了监视器,它具有使本地处理器缓存无效的效果,以便从主内存重新加载变量。然后,我们将能够看到以前版本可见的所有写入。

我还记得读过现代 Sun VM 上的非竞争同步很便宜。我对这种说法有点困惑。考虑如下代码:

class Foo {
    int x = 1;
    int y = 1;
    ..
    synchronized (aLock) {
        x = x + 1;
    }
}

对 x 的更新需要同步,但是获取锁是否也会从缓存中清除 y 的值?我无法想象会是这样,因为如果这是真的,像锁条带这样的技术可能无济于事。或者,JVM 是否可以可靠地分析代码以确保 y 不会在另一个使用相同锁的同步块中被修改,因此在进入同步块时不会将 y 的值转储到缓存中?

4

6 回答 6

46

简短的回答是JSR-133 的解释太过分了。这不是一个严重的问题,因为 JSR-133 是一个非规范性文档,它不是语言或 JVM 标准的一部分。相反,它只是一个文档,它解释了一种足以实现内存模型的可能策略,但通常不是必需的。最重要的是,关于“缓存刷新”的评论基本上是完全不合适的,因为基本上零架构将通过执行任何类型的“缓存刷新”来实现 Java 内存模型(许多架构甚至没有这样的指令)。

Java 内存模型是根据可见性、原子性、发生前关系等正式定义的,它准确地解释了哪些线程必须看到什么,哪些操作必须在其他操作和其他关系之前发生,使用精确(数学)定义模型。未正式定义的行为可能是随机的,或者在某些硬件和 JVM 实现上在实践中定义明确 - 但当然你不应该依赖它,因为它可能在未来发生变化,你永远无法确定除非您编写了 JVM 并且非常了解硬件语义,否则它一开始就已明确定义。

因此,您引用的文本并未正式描述 Java 保证什么,而是描述了一些具有非常弱的内存排序和可见性保证的假设架构如何使用缓存刷新满足 Java 内存模型要求。任何关于缓存刷新、主内存等的实际讨论显然不适用于 Java,因为这些概念不存在于抽象语言和内存模型规范中。

在实践中,内存模型提供的保证比完全刷新要弱得多——让每个原子的、与并发相关的或锁定操作都刷新整个缓存会非常昂贵——这在实践中几乎从未做过。相反,使用了特殊的原子 CPU 操作,有时与内存屏障指令结合使用,这有助于确保内存可见性和排序。因此,通过注意第一个是真的而第二个不是,可以解决廉价的非竞争同步和“完全刷新缓存”之间的明显不一致 - Java 内存模型不需要完全刷新(实际上也不会发生刷新)。

如果正式的内存模型有点难以消化(您不会孤单),您还可以通过查看Doug Lea 的食谱来更深入地研究这个主题,实际上链接在 JSR-133 常见问题解答中,但从具体的硬件角度来解决这个问题,因为它是为编译器编写者准备的。在那里,他们确切地讨论了特定操作需要哪些障碍,包括同步——并且在那里讨论的障碍可以很容易地映射到实际的硬件。大部分实际映射都在说明书中进行了讨论。

于 2009-12-07T23:24:06.607 回答
10

BeeOnRope 是对的,您引用的文本更多地深入研究了典型的实现细节,而不是 Java 内存模型确实保证的内容。在实践中,当您在 x 上同步时,您可能经常会看到 y 实际上已从 CPU 缓存中清除(此外,如果您的示例中的 x 是一个 volatile 变量,在这种情况下,不需要显式同步来触发效果)。这是因为在大多数 CPU 上(请注意,这是一种硬件效应,而不是 JMM 所描述的),缓存在称为缓存线的单元上工作,这些单元通常比机器字长(例如 64 字节宽)。由于只有完整的行可以在缓存中加载或失效,x 和 y 很有可能会落入同一行,并且刷新其中一个也会刷新另一行。

可以编写一个基准来显示这种效果。创建一个只有两个 volatile int 字段的类,并让两个线程执行一些操作(例如,在长循环中递增),一个在一个字段上,一个在另一个字段上。计时操作。然后,在两个原始字段之间插入 16 个 int 字段并重复测试(16*4=64)。请注意,数组只是一个引用,因此包含 16 个元素的数组不会起作用。您可能会看到性能显着提高,因为对一个字段的操作将不再影响另一个字段。这是否适合您将取决于 JVM 实现和处理器架构。我在 Sun JVM 和典型的 x64 笔记本电脑上实际看到了这一点,性能差异是数倍之多。

于 2012-02-22T20:45:53.417 回答
7

对 x 的更新需要同步,但是获取锁是否也会从缓存中清除 y 的值?我无法想象会是这样,因为如果这是真的,像锁条带这样的技术可能无济于事。

我不确定,但我认为答案可能是“是”。考虑一下:

class Foo {
    int x = 1;
    int y = 1;
    ..
    void bar() {
        synchronized (aLock) {
            x = x + 1;
        }
        y = y + 1;
    }
}

现在这段代码是不安全的,这取决于程序的其余部分发生了什么。但是,我认为内存模型意味着所y看到的值bar不应早于获取锁时的“真实”值。这意味着缓存必须为y和无效x

JVM 也可以可靠地分析代码以确保 y 不会在使用相同锁的另一个同步块中被修改?

如果锁是this,那么一旦所有的类都被预加载,这个分析看起来作为一个全局优化是可行的。(我并不是说这很容易,或者值得......)

在更一般的情况下,证明给定锁仅用于给定“拥有”实例的问题可能是棘手的。

于 2009-12-05T00:14:15.283 回答
4

我们是java开发者,我们只知道虚拟机,不知道真机!

让我对正在发生的事情进行推理——但我必须说我不知道​​我在说什么。

假设线程 A 运行在具有缓存 A 的 CPU A 上,线程 B 正在运行在具有缓存 B 的 CPU B 上,

  1. 线程 A 读取 y;CPU A 从主存中取出 y,并将值保存在缓存 A 中。

  2. 线程 B 为 'y' 分配新值。虚拟机此时不必更新主内存;就线程 B 而言,它可以在 'y' 的本地图像上读/写;也许'y'只不过是一个cpu寄存器。

  3. 线程 B 退出同步块并释放监视器。(它何时何地进入区块无关紧要)。到目前为止,线程 B 已经更新了很多变量,包括“y”。现在必须将所有这些更新写入主存储器。

  4. CPU B 将新的 y 值写入主存储器中。(我想)几乎立即,信息“主要 y 已更新”被连接到缓存 A,并且缓存 A 使其自己的 y 副本无效。这在硬件上一定发生得非常快。

  5. 线程 A 获取一个监视器并进入一个同步块——此时它不需要对缓存 A 做任何事情。'y'已经从缓存 A 中消失了。当线程 A 再次读取 y 时,它是从主内存中更新的B 分配的新值。

考虑另一个变量 z,它也在步骤 (1) 中由 A 缓存,但在步骤 (2) 中它没有被线程 B 更新。它可以在缓存 A 中一直存活到步骤(5)。对“z”的访问不会因为同步而减慢。

如果上述说法有道理,那么成本确实不是很高。


除了步骤(5)之外:线程 A 可能有自己的缓存,它甚至比缓存 A 还要快 - 例如,它可以为变量 'y' 使用寄存器。这不会被步骤(4)失效,因此在步骤(5)中,线程A必须在同步进入时擦除自己的缓存。不过,这不是一个巨大的惩罚。

于 2009-12-05T03:56:21.847 回答
4

您可能想查看 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() 方法可以形成happens-before关系。尤其:

  • 线程中的每个动作都发生在该线程中的每个动作之前,这些动作按程序的顺序出现在后面。
  • 监视器的解锁(同步块或方法退出)发生在同一监视器的每个后续锁定(同步块或方法入口)之前。并且由于happens-before关系是可传递的,因此线程在解锁之前的所有动作都发生在任何线程锁定该监视器之后的所有动作之前。
  • 对 volatile 字段的写入发生在对同一字段的每次后续读取之前。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'.

于 2013-02-27T22:48:19.103 回答
1

同步保证,只有一个线程可以进入一段代码。但它不能保证在同步部分中完成的变量修改对其他线程是可见的。只有进入同步块的线程才能保证看到更改。Java 中同步的内存效应可以与 C++ 中的双重检查锁定问题进行比较,Java 双重检查锁定被广泛引用并用作在多线程环境中实现延迟初始化的有效方法。不幸的是,当用 Java 实现时,它不能以独立于平台的方式可靠地工作,无需额外同步。当用其他语言(例如 C++)实现时,它取决于处理器的内存模型、编译器执行的重新排序以及编译器与同步库之间的交互。由于这些都没有在诸如 C++ 之类的语言中指定,因此几乎无法说明它将在哪些情况下工作。显式内存屏障可用于使其在 C++ 中工作,但这些屏障在 Java 中不可用。

于 2010-08-18T20:22:51.980 回答