为什么`synchronized(new Object()){}`a no-op?

yan*_*kee 53 java multithreading java-memory-model

在以下代码中:

class A {
    private int number;

    public void a() {
        number = 5;
    }

    public void b() {
        while(number == 0) {
            // ...
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

这听起来像我只需要两个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()) {}
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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

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

synchronized (new Object()) {}
Run Code Online (Sandbox Code Playgroud)

这实际上是一个无操作,并且您的编译器可以完全删除它,因为编译器知道没有其他线程将在同一监视器上同步.您必须为一个线程设置一个before-before关系,以查看另一个线程的结果.

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

ysh*_*vit 49

常见问题解答不是此事的权威; JLS是.第17.4.4节规定了同步关系,它们提供了先发生关系(17.4.5).相关要点是:

  • 显示器上的解锁动作 同步-与上所有后续锁定动作(其中,"随后的"是根据该同步命令定义).

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

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

  • 除了能够识别纯粹本地对象的Escape Analysis之外,还有其他代码转换可能会破坏无效同步的全局缓存刷新效果.例如,*扩展*synchronized`语句的受保护区域是合法的,以将多个后续同步或循环的多次迭代合并为一个,从而减少获取和释放操作的数量.然后,由于没有其他线程可以获取其间的锁定,因此不存在*before-before*关系,并且在仍然保持锁定时不需要刷新. (11认同)
  • 这意味着从理论上讲,我们根本不需要刷新缓存.但是我们必须确保在同一个监视器上同步的另一个线程可以看到发生的所有事情,直到前一个线程离开同步块,并且最简单的方法是保证这一点是在输入和写入缓存上刷新读取缓存出口.正确? (5认同)
  • @yankee常见问题解答说,笨拙和愚蠢的IMO,就是如果有这样的缓存,并且刷新缓存是使监视器工作所需的,因为它们必须工作,那么缓存将被刷新.为什么这是一个聪明的事情,我不知道.它导致IMO比理解更多的误解. (2认同)

Sol*_*low 20

以下模式,有些人用来强制内存屏障,不起作用:

它不能保证是无操作,但规范允许它成为无操作.当两个线程在同一个对象上同步时,规范只需要同步以在两个线程之间建立一个先发生的关系,但实际上更容易实现一个JVM,其中对象的标识无关紧要.

我认为synchronized-Statement将导致缓存刷新

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

  • @yankee编译器不必在天花板上抛鱼.这并不意味着Java中有鱼或天花板. (6认同)
  • @yankee假设有寄存器,并且寄存器中的内容被缓存,这不是必需的.当然,编译器永远不需要做任何事情,但符合标准.如果编译器使用寄存器,则只有在符合标准的实际要求时才需要刷新它们.除非要求符合标准,否则编译器不必执行任何操作. (2认同)