代码之家  ›  专栏  ›  技术社区  ›  Binil Thomas

Java中同步的内存效应

  •  38
  • Binil Thomas  · 技术社区  · 15 年前

    JSR-133 FAQ 说:

    但是还有更多的东西需要同步 而不是相互排斥。同步 确保内存由线程写入 同步块之前或期间 在可预测的 其他螺纹的方式 在同一个监视器上同步。后 我们退出一个同步块,我们 释放监视器,它具有 将缓存刷新到主缓存的效果 内存,以便写入 其他人可以看到线程 线程。在我们进入 同步块,我们获得 监视器,其作用是 使本地处理器缓存无效 以便重新加载变量 从主存储器。我们就可以 查看所有可见的写入 上一版本。

    我还记得我读过一篇文章,在现代的sun vms上,未经处理的同步是便宜的。我对这个说法有点困惑。考虑代码如下:

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

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

    7 回复  |  直到 6 年前
        1
  •  41
  •   BeeOnRope    6 年前

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

    Java内存模型是根据可见性、原子性、关系发生之前等形式来定义的,它确切地解释了什么线程。 必须 看什么,什么行动 必须 在使用精确(数学)定义的模型进行其他操作和其他关系之前发生。未被正式定义的行为可能是随机的,或者在某些硬件和JVM实现的实践中被很好地定义——但是当然,您不应该依赖于这一点,因为它在将来可能会发生变化,而且除非您编写了JVM并且很好地意识到硬件语义,否则您永远无法真正确定它在一开始就被很好地定义了。抽搐。

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

    实际上,内存模型提供的保证要比完全刷新弱得多——让每个原子的、与并发性相关的或锁操作刷新整个缓存将是非常昂贵的——而且这在实践中几乎从未实现过。相反,使用特殊的原子CPU操作,有时与 memory barrier 指令,有助于确保内存的可见性和顺序。因此,廉价的无争用同步和“完全刷新高速缓存”之间的明显不一致是通过注意第一个是真的,第二个不是——Java内存模型不需要完全刷新(并且在实践中没有发生刷新)。

    如果正式的记忆模型有点太重而无法消化(你不会孤单一人),你也可以通过看一眼深入研究这个主题。 Doug Lea's cookbook 它实际上在JSR-133 FAQ中链接,但从具体的硬件角度来看,它是针对编译器编写者的。在那里,他们确切地讨论了特定操作所需的屏障,包括同步——这里讨论的屏障可以很容易地映射到实际的硬件上。很多实际的映射都在食谱中讨论过。

        2
  •  9
  •   Michał Kosmulski    12 年前

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

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

        3
  •  7
  •   Stephen C    15 年前

    更新到x需要同步, 但是锁的获得 也从 隐藏物?我不能想象那是 因为如果是真的, 像锁条这样的技术可能 无济于事。

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

    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 ,此分析看起来在所有类都预加载之后作为全局优化是可行的。(我不是说这很容易,也不值得……)

    在更一般的情况下,证明一个给定的锁只与一个给定的“拥有”实例一起使用的问题可能是难以解决的。

        4
  •  4
  •   irreputable    15 年前

    我们是Java开发者,我们只知道虚拟机,而不是真正的机器!

    让我理论化正在发生的事情——但我必须说我不知道我在说什么。

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

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

    2. 线程B将新值赋给“Y”。此时,VM不需要更新主内存;就线程B而言,它可以在“y”的本地映像上读/写;“y”可能只是一个CPU寄存器。

    3. 线程B退出同步块并释放监视器。(它何时何地进入街区无关紧要)。线程B更新了相当多的变量,直到现在,包括“y”。所有这些更新必须立即写入主内存。

    4. CPU B将新的Y值写入主内存中的“Y”。(我想)几乎是瞬间,“主Y被更新”的信息被连接到缓存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必须在进入同步时清除自己的缓存。不过,这不是一个巨大的惩罚。

        5
  •  3
  •   rohit kochar    11 年前

    您可能需要检查JDK6.0文档 http://java.sun.com/javase/6/docs/api/java/util/concurrent/package-summary.html#MemoryVisibility

    内存一致性属性 Java语言规范的第17章定义了在内存操作(如共享变量的读写)之前发生的关系。只有在写操作发生在读操作之前,一个线程的写操作结果才保证对另一个线程的读操作可见。同步和易失性构造以及thread.start()和thread.join()方法可以在关系之前形成。特别地:

    • 线程中的每一个操作都发生在该线程中的每一个操作之前,该操作随后按程序的顺序出现。
    • 监视器的解锁(同步块或方法出口)发生在同一监视器的每个后续锁(同步块或方法入口)之前。并且因为发生在关系可传递之前,所以线程在解锁之前的所有操作都发生在该监视器的任何线程锁定之后的所有操作之前。
    • 对易失性字段的写入发生在随后对该字段的每次读取之前。易变字段的写入和读取与进入和退出监视器具有类似的内存一致性效果,但不需要互斥锁定。
    • 在已启动线程中的任何操作之前,对线程启动的调用将发生。
    • 线程中的所有操作都发生在其他线程成功从该线程上的联接返回之前。

    因此,正如上面突出显示的点所述:在监视器上发生解锁之前发生的所有更改对所有锁定的线程(以及在那里自己的同步块)都是可见的。 同一个监视器。这与Java在语义之前发生的情况是一致的。 因此,当其他线程在“alock”上获取监视器时,对y所做的所有更改也将刷新到主内存中。

        6
  •  1
  •   Sudhakar Kalmari    14 年前

    同步保证只有一个线程可以输入一个代码块。但它不能保证在同步部分内所做的变量修改对其他线程是可见的。只有进入同步块的线程才能保证看到更改。 Java中的同步内存效应可与C++和Java双重检查锁定问题进行比较 双校验锁是在多线程环境中实现延迟初始化的一种有效方法,被广泛引用。不幸的是,它 在Java中实现时,不会以平台无关的方式可靠地工作 ,而不进行其他同步。当以其他语言(如C++)实现时,它取决于处理器的内存模型、编译器执行的重新排序以及编译器和同步库之间的交互。由于这些语言中没有一种是用C++语言来指定的,所以很少有人会谈到它将工作的情况。显式内存障碍可以用来使它在C++中工作,但是这些障碍在Java中是不可用的。

        7
  •  -1
  •   Jeremy Raymond    15 年前

    由于y超出了synchronized方法的范围,因此无法保证它的更改在其他线程中可见。如果要确保在所有线程中看到对y的更改都是相同的,那么 全部的 线程在读/写Y时必须使用同步。

    如果某些线程以同步方式更改y,而其他线程不更改y,那么您将得到意外的行为。必须同步线程之间共享的所有可变状态,才能保证看到线程之间的更改。必须同步所有线程上对共享可变状态(变量)的所有访问。

    是的,JVM保证在持有锁的同时,没有其他线程可以进入受同一锁保护的代码区域。