【问题标题】:Why is `synchronized (new Object()) {}` a no-op?为什么 `synchronized (new Object()) {}` 是无操作的?
【发布时间】:2016-09-05 15:51:44
【问题描述】:

在以下代码中:

class A {
    private int number;

    public void a() {
        number = 5;
    }

    public void b() {
        while(number == 0) {
            // ...
        }
    }
}

如果方法 b 被调用,然后启动了一个新线程来触发方法 a,那么方法 b 不能保证看到 number 的变化,因此 b 可能永远不会终止。

当然,我们可以通过 number volatile 来解决这个问题。但是,出于学术原因,我们假设 volatile 不是一个选项:

JSR-133 FAQs 告诉我们:

退出同步块后,我们释放监视器,它具有将缓存刷新到主内存的效果,因此该线程所做的写入可以对其他线程可见。在我们进入同步块之前,我们获取了监视器,它具有使本地处理器缓存无效的效果,以便从主内存重新加载变量。

这听起来我只需要ab 进入和退出任何synchronized-Block,无论他们使用什么监视器。更准确地说,它听起来像这样......:

class A {
    private int number;

    public void a() {
        number = 5;
        synchronized(new Object()) {}
    }

    public void b() {
        while(number == 0) {
            // ...
            synchronized(new Object()) {}
        }
    }
}

...将消除问题并保证b 将看到更改为a,因此也将最终终止。

但常见问题解答也明确指出:

另一个含义是下面的模式,有些人 用于强制内存屏障,不起作用:

synchronized (new Object()) {}

这实际上是一个空操作,您的编译器可以完全删除它, 因为编译器知道没有其他线程会同步 同一个显示器。您必须为 一个线程查看另一个线程的结果。

现在这令人困惑。我认为同步语句会导致缓存刷新。它肯定不能将缓存刷新到主内存,因为主内存中的更改只能由在同一监视器上同步的线程看到,特别是因为对于基本上做同样事情的 volatile 我们甚至不需要监视器,还是我弄错了?那么为什么这是一个无操作并且不会导致b 被保证终止呢?

【问题讨论】:

  • 引用未保存,因此没有其他线程能够等待该引用。你想保护什么?
  • 这个问题完美地说明了我敦促人们不要试图解释或理解语言语义的原因,这些内容可能存在也可能不存在,例如虚构的“本地处理器缓存”。
  • @DavidSchwartz 没错,实际上“缓存刷新”这个短语,尤其是在“主内存”这个短语附近时,可能会对实际发生的事情产生很大的误导。高速缓存一致性协议通常可以确保内存一致性,而无需实际到达慢速主内存。而且,如果您放弃 JLS 和 JMM 并尝试对底层架构进行推理,则需要考虑诸如某些同步机制不能确保全局内存一致性(由 IRIW 演示)之类的事实。所以,就像 yshavit 说的,坚持使用 JLS 才是正确的选择。

标签: java multithreading java-memory-model


【解决方案1】:

FAQ 不是该问题的权威; JLS 是。 17.4.4 部分指定了同步关系,它馈入发生前的关系 (17.4.5)。相关的要点是:

  • 监视器m上的解锁操作同步m上的所有后续锁定操作(其中“后续”根据同步定义顺序)。

由于这里的m是对new Object()的引用,并且它从未存储或发布到任何其他线程,我们可以确定没有其他线程会获得m上的锁 在此块中的锁被释放后。此外,由于 m 是一个新对象,我们可以确定之前没有任何动作在它上面解锁。因此,我们可以确定没有任何动作与该动作正式同步。

从技术上讲,您甚至不需要执行完整的缓存刷新即可达到 JLS 规范;这超出了 JLS 的要求。 典型 实现可以做到这一点,因为这是硬件让你做的最简单的事情,但可以说它“超越”了。在escape analysis 告诉优化编译器我们需要更少的情况下,编译器可以执行更少。在您的示例中,转义分析可以告诉编译器该操作无效(由于上述推理)并且可以完全优化。

【讨论】:

  • 所以这意味着理论上我们根本不需要刷新缓存。但是我们必须确保在同一个监视器上同步的另一个线程可以看到在前一个线程离开同步块之前发生的所有事情,并且保证这一点的最简单方法是在进入时刷新读取缓存和写入缓存出口。对吗?
  • @yankee 常见问题解答的意思是,笨拙和愚蠢的 IMO,如果有这样的缓存,并且刷新该缓存是使监视器正常工作所需要的,那么该缓存会被冲洗掉。为什么这么说很聪明,我不知道。国际海事组织,它导致的误解远多于理解。
  • 除了能够识别纯本地对象的转义分析之外,还有其他代码转换可能会破坏无效同步的全局缓存刷新效果。例如,扩展 synchronized 语句的受保护区域以将多个后续同步或循环的多次迭代合并为一个是合法的,从而减少了获取和释放操作的数量。然后,由于没有其他线程可以在中间获得锁,因此不存在 happens-before 关系,并且在仍然持有锁的同时不需要刷新。
  • @Holger 对于这样的问题,这里有 5-10 个人,我在尝试理解时会立即搜索。 :) 你就是其中之一。谢谢您的意见
【解决方案2】:

以下模式(某些人用来强制设置内存屏障)不起作用:

不能保证它是无操作的,但规范允许它是无操作的。当两个线程在同一个对象上同步时,规范只需要同步来建立两个线程之间的happens-before关系,但实际上实现一个对象身份无关紧要的JVM会更容易。

我认为同步语句会导致缓存刷新

Java 语言规范中没有“缓存”。这个概念只存在于某些(好吧,几乎所有)硬件平台和 JVM 实现的细节中。

【讨论】:

  • 所以这基本上意味着规范允许推迟同步,直到同一监视器上发生另一个同步操作?
  • > Java 语言规范中没有“缓存””实际上 JLS 在chapter 17 中说“编译器不必刷新缓存在寄存器中的写入”。听起来像他们使用寄存器作为缓存,所以 JLS 中有缓存。
  • @Yankee 这是 JLS 的非内存模型相关部分的解释。如果您阅读 JMM 的实际定义,您将找不到任何对缓存或寄存器的引用(可能在非规范性解释部分除外)。
  • @yankee 假设有寄存器并且东西缓存在寄存器中,这不是必需的。当然,编译器除了遵守标准外,从不需要做任何事情。如果编译器使用寄存器,则仅在需要符合标准的实际要求时才需要刷新它们。除非需要符合标准,否则编译器无需执行任何操作。
  • @yankee 编译器不必在天花板上扔鱼。这并不意味着 Java 中有鱼或天花板。
猜你喜欢
  • 2012-06-02
  • 2013-05-18
  • 2011-07-13
  • 2014-06-21
  • 1970-01-01
  • 2018-09-03
  • 2014-02-10
  • 1970-01-01
  • 2014-04-24
相关资源
最近更新 更多