27

我正在查看有关我的代码库的Findbugs报告,并且触发的模式之一是针对空synchronzied块(即synchronized (var) {})。文档说:

空同步块比大多数人认识到的要微妙和难以正确使用,并且空同步块几乎从来都不是比不那么做作的解决方案更好的解决方案。

在我的情况下,它发生是因为块的内容已被注释掉,但synchronized语句仍然存在。在什么情况下空synchronized块可以实现正确的线程语义?

4

5 回答 5

18

一个空的同步块将等到没有其他人使用该监视器。

这可能是您想要的,但是因为您没有保护同步块中的后续代码,所以没有什么可以阻止其他人在您运行后续代码时修改您正在等待的内容。这几乎不是你想要的。

于 2009-03-26T16:10:37.163 回答
17

我认为早期的答案未能强调关于空synchronized块的最有用的事情:跨线程公开变量更改和其他操作。正如jtahlborn 所指出的,同步通过在编译器上施加内存屏障来实现这一点。不过,我没有找到 SnakE 应该在哪里讨论这个问题,所以在这里我解释一下我的意思。

int variable;

void test() // This code is INCORRECT
{
    new Thread( () ->  // A
    {
        variable = 9;
        for( ;; )
        {
            // Do other stuff
        }
    }).start();

    new Thread( () ->  // B
    {
        for( ;; )
        {
            if( variable == 9 ) System.exit( 0 );
        }
    }).start();
}

上面的代码不正确。编译器可能会隔离线程 A 对变量的更改,从而有效地将其隐藏在 B 中,然后它将永远循环。

使用空synchronized块来暴露跨线程的更改

一种更正是向volatile变量添加修饰符。但这可能效率低下;它强制编译器公开所有更改,其中可能包括不感兴趣的中间值。synchronized另一方面,空块仅在关键点公开更改的值。例如:

int variable;

void test() // Corrected version
{
    new Thread( () ->  // A
    {
        variable = 9;
        synchronized( o ) {} // Force exposure of the change
        for( ;; )
        {
            // Do other stuff
        }
    }).start();

    new Thread( () ->  // B
    {
        for( ;; )
        {
            synchronized( o ) {} // Look for exposed changes
            if( variable == 9 ) System.exit( 0 );
        }
    }).start();
}

final Object o = new Object();

内存模型如何保证可见性

两个线程必须在同一个对象上同步以保证可见性。该保证依赖于Java 内存模型,特别是“监视器 m 上的解锁操作与 m上的所有后续锁定操作同步”并因此发生在这些操作之前的规则。因此,在 A 的synchronized块尾部解锁 o 的监视器发生在B 块头部的最终锁定之前。并且因为 A 的write先于其解锁而 B 的 lock 先于其read,所以保证扩展到包括 write 和 read —— write happens-before read —— 使得修改后的程序在内存模型方面是正确的。

我认为这是空synchronized块最重要的用途。

于 2015-08-11T04:31:25.483 回答
5

过去的情况是,规范暗示发生了某些内存屏障操作。但是,规范现在已经更改,原始规范从未正确实施。它可以用来等待另一个线程释放锁,但是协调另一个线程已经获得锁会很棘手。

于 2009-03-26T17:23:23.743 回答
4

同步不仅仅是等待,而不优雅的编码可以达到所需的效果。

来自http://www.javaperformancetuning.com/news/qotm030.shtml

  1. 线程获取对象 this 的监视器上的锁(假设监视器已解锁,否则线程等待直到监视器解锁)。
  2. 线程内存刷新其所有变量,即它的所有变量都有效地从“主”内存读取(JVM 可以使用脏集来优化这一点,以便仅刷新“脏”变量,但从概念上讲这是相同的。参见章节Java 语言规范的第 17.9 节)。
  3. 代码块被执行(在这种情况下,将返回值设置为 i3 的当前值,它可能刚刚从“主”内存中重置)。
  4. (对变量的任何更改现在通常都会写入“主”内存,但对于 geti3() 我们没有任何更改。)
  5. 线程释放对象 this 的监视器上的锁。
于 2009-03-30T06:45:01.607 回答
0

如需深入了解 Java 的内存模型,请观看 Google 的“编程语言高级主题”系列视频: http ://www.youtube.com/watch?v=1FX4zco0ziY

它很好地概述了编译器可以(通常在理论上,但有时在实践中)对您的代码执行的操作。对于任何认真的 Java 程序员来说都是必不可少的东西!

于 2009-03-26T18:58:21.253 回答