在什么情况下,空的同步块可以实现正确的线程语义?

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

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

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


答案 1

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

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


答案 2

我认为前面的答案没有强调空块最有用的东西:跨线程公开变量更改和其他操作。正如 jtahlborn 所指出的,同步是通过对编译器施加内存屏障来实现此目的的。不过,我没有找到SnakeE应该在哪里讨论这个问题,所以在这里我解释了我的意思。synchronized

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 中,然后 B 将永远循环。

使用空块公开跨线程的更改synchronized

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

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块的尾部解锁o的监视器发生在B块的头部的最终锁定之前。而且,由于A的写入先于其解锁,B的锁定先于其读取,因此保证扩展到涵盖写入和读取 - 写入发生 - 在读取之前 - 使修改后的程序在内存模型方面正确。synchronized

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