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

sk.*_*sk. 25 java multithreading findbugs synchronized

我正在查看关于我的代码库的Findbugs报告,其中一个触发的模式是空synchronzied块(即synchronized (var) {}).该文件说:

与大多数人认识到的相比,空的同步块更加微妙且难以正确使用,并且空的同步块几乎不是比较少设计的解决方案更好的解决方案.

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

Pau*_*lin 18

空的同步块将等待,直到没有其他人使用该同步器.这可能是你想要的,但是因为你没有保护synchronized块中的后续代码,所以没有什么能阻止别人修改你在运行后续代码时所等待的内容.这几乎不是你想要的.

  • 另一个重要的用法是它就像一个内存屏障(比如读/写一个volatile变量),A @ SnakE在下面讨论. (7认同)
  • 究竟.我有一种方法可以让一些线程像工人一样,其他线程就像消费者一样.所有消费者都使用空的`synchronized`来等待工人完成修改实例,从那时起 - 不需要进一步同步,所以所有的读取都在同步代码之外完成.我相信`synchronized`是手动管理锁实例的更清晰的解决方案. (2认同)

Mic*_*lan 13

前面的答案没有强调空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();
}
Run Code Online (Sandbox Code Playgroud)

以上程序不正确.变量的值可以在线程A或B中本地缓存,或者两者都缓存.因此B可能永远不会读取A写入的9的值,因此可能永远循环.

通过使用空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();
Run Code Online (Sandbox Code Playgroud)

内存模型如何保证可见性

两个线程必须在同一个对象上同步才能保证可见性.这种保证依赖于Java内存模型,特别是"监视器上的解锁操作与m 上的所有后续锁定操作同步 "的规则,从而这些操作之前发生.所以A在其synchronized区块尾部的o监视器的解锁发生 - 在B随后锁定其区块的头部之前.(注意,关系的这个奇怪的尾部顺序解释了为什么主体可以为空.)还有A的写入在其解锁之前且B的锁在其读取之前,关系必须扩展以覆盖写入和读取:写入发生在读之前.正是这种关键的,扩展的关系使得修改后的程序在内存模型方面是正确的.

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


   *我说话好像是处理器缓存的问题,因为我认为这是一种有用的查看方式.事实上,正如Aleksandr Dubinsky评论的那样,"所有现代处理器都是缓存一致的.之前发生的关系更多地是关于允许编译器做什么而不是CPU.

  • 这是一个很大的进步。但有一点需要考虑:操作仍然可能按以下顺序执行:A:写入,B:同步块,A:同步块,B:读取。那么,不存在*happens-before*关系,但写入的值仍然可以通过 racy 读取被 B 感知到。对于这个特定的示例来说,这并不重要,因为它只是一个原子 int 变量(但是,有很多替代方案,“易失性”、“原子整数”或“VarHandle”)。但在任何涉及多个变量的情况下,这一点都会被打破。 (2认同)
  • 谨防。`if( state.toExit ) exit( state.exitValue );` 不保证在这里工作,因为这是对 `state` 的两次读取,没有线程间语义。如果没有正确的同步,读取和写入可能会被感知为无序,因此“if”条件内的第一次读取可能会感知到比“exit(…)”调用语句内的读取更新的值。您必须首先将引用读入局部变量(这又是一种替代方案“易失性”、“原子整数”或“VarHandle”比空的“同步”块工作得更好的情况)。 (2认同)

Tom*_*ine 5

过去,规范暗示某些内存屏障操作发生.但是,规范现在已经改变,原始规范从未正确实现.它可能用于等待另一个线程释放锁,但协调另一个线程已经获得锁定将是棘手的.