为什么 'synchronized (new Object()) {}' 是 no-op?

2022-08-31 19:36:22

在下面的代码中:

class A {
    private int number;

    public void a() {
        number = 5;
    }

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

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

当然,我们可以解决这个问题。但是,出于学术原因,让我们假设这不是一种选择:numbervolatilevolatile

JSR-133 常见问题解答告诉我们:

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

这听起来好像我只需要两者,就可以进入和退出任何-Block,无论他们使用什么显示器。更准确地说,它听起来像这样...:absynchronized

class A {
    private int number;

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

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

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

然而,常见问题解答也明确指出:

另一个含义是,以下模式(有些人用来强制设置内存屏障)不起作用:

synchronized (new Object()) {}

这实际上是一个 no-op,编译器可以完全删除它,因为编译器知道没有其他线程将在同一监视器上同步。您必须为一个线程设置“发生前”关系才能看到另一个线程的结果。

现在这令人困惑。我以为同步语句会导致缓存刷新。它肯定不能将缓存刷新到主内存,因为主内存中的更改只能由在同一监视器上同步的线程看到,特别是因为对于基本上执行相同操作的易失性,我们甚至不需要监视器,或者我在那里错了吗?那么,为什么这是一个禁止操作并且不会导致保证终止呢?b


答案 1

常见问题解答不是此事的权威;JLS是。第 17.4.4 节指定了与之同步的关系,这些关系馈送到了发生之前的关系 (17.4.5)。相关的要点是:

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

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

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


答案 2

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

它不能保证是无操作,但规范允许它是无操作。当两个线程在同一对象上同步时,规范仅要求同步以在两个线程之间建立发生之前的关系,但实际上,在对象的标识无关紧要的情况下实现 JVM 会更容易。

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

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