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

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

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

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

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

前面的答案未能强调关于空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中本地缓存,或者两者都缓存。 因此B可能永远不会读取A写入的值9,因此可能永远循环。

通过使用空的synchronized块,使线程间的变量可见变化

一种可能的修正是向变量添加volatile (有效“无缓存”)修饰符。 然而,有时这是低效的,因为它完全禁止缓存变量。 另一方面,清空synchronized块不禁止缓存。 他们所做的就是强制缓存在某些关键点与主存储器同步。 例如:

 int variable; void test() // revised { new Thread( () -> // A { variable = 9; synchronized( o ) {} // flush to main memory for( ;; ) { // do other stuff } }).start(); new Thread( () -> // B { for( ;; ) { synchronized( o ) {} // refresh from main memory if( variable == 9 ) System.exit( 0 ); } }).start(); } final Object o = new Object(); 

内存模型如何保证可见性

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

比挥发性更深的效果

synchronized块具有比volatile更深的效果。 假设变量不是原始整数,如上所示,而是复合的可变对象。 假设其内容由线程A( Pius描述的工作线程)逐步添加,随后由线程B(消费者)读取。 然后声明变量volatile将不再足以纠正程序,因为volatile修饰符的效果不会扩展到变量的内容。 但是,添加空的synchronized块,如上所示,仍然会纠正它。

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

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

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

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

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

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

它非常清晰地概述了编译器(通常在理论上,但有时在实践中)可以对代码执行的操作。 任何严肃的Java程序员必不可少的东西!