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

Posted

技术标签:

【中文标题】在啥情况下,一个空的同步块可以实现正确的线程语义?【英文标题】:In what situations could an empty synchronized block achieve correct threading semantics?在什么情况下,一个空的同步块可以实现正确的线程语义? 【发布时间】:2010-10-15 17:52:04 【问题描述】:

我正在查看关于我的代码库的Findbugs 报告,其中触发的模式之一是空的synchronzied 块(即synchronized (var) )。 documentation says:

空同步块远不止这些 微妙且难以正确使用 大多数人都认识,并且空虚 同步块几乎从不 比不那么做作更好的解决方案 解决方案。

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

【问题讨论】:

【参考方案1】:

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

【讨论】:

旁注:我肯定会用 java.util.concurrent 类之一替换空同步块的概念。 Locks/Barries/Latches 都可以很好地解决这个问题,并且从使用上总是很明确的意思(与神奇的空大括号相反) 另一个重要的用法是它就像一个内存屏障(比如读/写一个易失性变量),@SnakE 在下面讨论。 没错。我有一种方法可以让一些线程像工作者一样,而其他线程像消费者一样。所有消费者所做的就是使用空的synchronized 等待工作人员完成修改实例,从那时起 - 不需要进一步同步,因此所有读取都在同步代码之外完成。我相信synchronized 是比手动管理锁实例更清晰的解决方案。 @Pius,在您阅读实例时,其他工作人员是否不可能修改实例? @Paul Tomblin No. Worker 是第一个同步实例的,一旦它释放它,就没有其他线程修改它。这是一个非常具体的案例,我还没有在其他任何地方应用过。【参考方案2】:

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

【讨论】:

我认为规范对内存屏障(排序约束)是明确的,至少从 2004 年引入的新内存模型开始。我自己的答案引用了这个。【参考方案3】:

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

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

【讨论】:

【参考方案4】:

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

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

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

【讨论】:

这是对真实规则的危险简化。同步块不会“将其变量刷新到(全局)内存”。唯一的保证是,如果线程 A 在特定对象上同步,然后线程 B 稍后在同一个对象上同步,那么线程 B 将看到线程 A 的更改。【参考方案5】:

我认为早期的答案未能强调关于空 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 memory model,特别是“对监视器 m 的解锁操作同步所有后续对 m 的锁定操作”并因此发生之前 那些动作。因此,在 A 的 synchronized 块的尾部解锁 o 的监视器发生在最终锁定在 B 的块的头部。并且因为 A 的 write 在其解锁之前,而 B 的锁在其 read 之前,所以保证扩展到包括 write 和 read —— write happens-before read — 使修改后的程序在内存模型方面正确。

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

【讨论】:

“volatile 修饰符的效果不会扩展到变量的内容”,这是一种非常混乱的语言。我认为您的意思是两个线程 reading volatile 不会创建发生前的关系。但是,写入和读取(如果读取成功读取写入)确实会创建这样的关系。发生之前的关系延伸到线程完成的所有事情。 此外,所有现代处理器都是缓存一致的。之前发生的关系更多的是关于允许编译器做什么而不是 CPU。 @Aleksandr,我更正了答案 - 再次 - 这次完全删除了误导性的“缓存”引用。

以上是关于在啥情况下,一个空的同步块可以实现正确的线程语义?的主要内容,如果未能解决你的问题,请参考以下文章

重新认识synchronized(下)

java中几种Map在啥情况下使用?

Internet Explorer 在啥情况下无法正确卸载 ActiveX 控件?

线程的同步机制:同步代码块&同步方法

Java多线程5:synchronized锁方法块

C语言中volatile在啥情况下使用