在什么情况下空的同步块可以实现正确的线程语义?
我正在查看关于我的代码库的 Findbugs 报告,触发的模式之一是空 < code>synchronzied 块(即 synchronized (var) {}
)。 文档说:
空同步块要多得多 微妙且难以正确使用 大多数人都认得,而且空虚 同步块几乎从来都不是 比不那么做作的更好的解决方案 解决方案。
就我而言,发生这种情况是因为块的内容已被注释掉,但 synchronized
语句仍然存在。 在什么情况下,空的同步块可以实现正确的线程语义?
I was looking through a Findbugs report on my code base and one of the patterns that was triggered was for an empty synchronzied
block (i.e. synchronized (var) {}
). The documentation says:
Empty synchronized blocks are far more
subtle and hard to use correctly than
most people recognize, and empty
synchronized blocks are almost never a
better solution than less contrived
solutions.
In my case it occurred because the contents of the block had been commented out, but the synchronized
statement was still there. In what situations could an empty synchronized
block achieve correct threading semantics?
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(5)
空的同步块将等待,直到没有其他人使用该监视器。
这可能就是您想要的,但是因为您没有保护同步块中的后续代码,所以没有什么可以阻止其他人在运行后续代码时修改您正在等待的内容。 这几乎从来都不是你想要的。
An empty synchronized block will wait until nobody else is using that monitor.
That may be what you want, but because you haven't protected the subsequent code in the synchronized block, nothing is stopping somebody else from modifying what ever it was you were waiting for while you run the subsequent code. That's almost never what you want.
我在第一节和第二节中解释了空的同步块如何“实现正确的线程”。 我将在第三节中解释它如何成为“更好的解决方案”。 我在第四节也是最后一节中通过示例展示了它如何“微妙且难以正确使用”。
1. 正确的线程
在什么情况下,空的
synchronized
块可以启用正确的线程?考虑一个例子。
上面的代码不正确。 运行时可能会隔离线程 A 对布尔变量 toExit 的更改,从而有效地对 B 隐藏它,然后 B 就会永远循环。
它可以通过引入空的同步块来纠正,如下所示。
2. 正确性的基础
空的
synchronized
块如何使代码正确?Java 内存模型保证“监视器 m 上的解锁操作与 m 上的所有后续锁定操作同步”,从而发生在这些操作之前 (§17.4.4)。 因此,A 的
synchronized
块尾部的监视器o
的解锁发生在其最终的锁定之前位于 B 的synchronized
块的头部。 由于 A 对变量的写入先于其解锁,而 B 的锁定先于读取,因此保证扩展到写入和读取操作:写入发生在读取之前< /em>.现在,“[如果]一个操作发生在另一个操作之前,则第一个操作对第二个操作可见并且在第二个操作之前排序”(§17.4.5)。 正是这种可见性保证使得代码在内存模型方面是正确的。
3. 比较实用性
空的
synchronized
块如何成为比替代方案更好的解决方案?与非空“同步”块的比较
一种替代方案是非空“同步”块。 非空同步块有两个作用:a)它提供上一节中描述的排序和可见性保证,有效地强制在同一监视器上同步的所有线程之间公开内存更改; b) 它使块内的代码在这些线程中有效地原子化; 该代码的执行不会与其他块同步代码的执行交错。
空的
synchronized
块仅执行上述 (a) 操作。 在仅需要 (a) 且 (b) 可能产生大量成本的情况下,空的同步块可能是更好的解决方案。与“易失性”修饰符
另一种选择是附加到特定变量声明的
易失性
修饰符,从而强制公开其更改。 空的synchronized
块的不同之处在于,它不应用于任何特定变量,而是应用于所有变量。 在大量变量发生需要公开的更改的情况下,空的同步块可能是更好的解决方案。此外,易失性修饰符强制公开对变量的每个单独写入,从而在所有线程中公开每个写入。 空的同步块在暴露时间(仅当块执行时)和范围(仅对于在同一监视器上同步的线程)方面都不同。 在时间和范围范围更窄的情况下可能会带来显着的成本效益,因此空的同步块可能是更好的解决方案。
4.重新审视正确性
并发编程是困难的。 因此,空的同步块可能“微妙且难以正确使用”也就不足为奇了。 下面的示例显示了一种滥用它们的方法(由 Holger 提到)。
线程 B 的语句“
if( toExit ) exit( exitValue )
”假设两个变量之间存在同步,但代码并不保证这一点。 假设 B 碰巧在 A 写入toExit
和exitValue
之后,但在后续执行两个synchronized
语句之前(A 然后 B 的)。 那么 B 看到的可能是第一个变量的更新值 (true
) 以及第二个变量的 un 更新值 (零),导致它退出错误的值。纠正代码的一种方法是通过最终字段的调解。
修改后的代码是正确的,因为 Java 内存模型保证,当 B 读取 A 写入的
state
新值时,它将看到最终字段toExit
完全初始化的值> 和exitValue
,两者在State
的声明中都是隐式最终的。 “只有在对象完全初始化后才能看到对该对象的引用的线程保证看到该对象的最终字段的正确初始化值。” (§17.5)对于这项技术的通用性至关重要(尽管与本示例无关),规范继续说道:“它还将看到由那些最终字段引用的任何对象或数组的版本,这些版本至少是最新的正如最后的字段一样。 因此,同步的保证深入到数据结构中。
。
当已知
state
仅更改一次时,线程 B 对state
变量的本地缓存(上面的示例)可能似乎没有必要 虽然它有其原始值,但语句“if( state.toExit ) exit( state.exitValue )
”将短路并仅读取一次; 否则它将有其最终值并且保证在两次读取之间不会改变。 但正如霍尔格指出的那样,并没有这样的保证。考虑一下如果我们不考虑缓存会发生什么。
“只要程序的所有结果执行产生可以由内存模型预测的结果,实现就可以自由地生成它喜欢的任何代码。 这为实现者提供了很大的自由来执行无数的代码转换,包括重新排序操作和删除不必要的同步。 (§17.4)
因此,看到“
if( state.toExit ) exit( state.exitValue )
”位于同步块之外,并且state
是一个非易失性变量,以下转换将是有效的。这实际上可能就是代码的执行方式。 然后,第一次读取
state
(进入s
)可能会产生其原始值,而下一次读取会产生其最终值,导致程序意外退出,值为 0而不是 1。How an empty
synchronized
block can ‘achieve correct threading’ I explain in sections one and two. How it can be a ‘better solution’ I explain in section three. How it can nevertheless be ‘subtle and hard to use correctly’ I show by example in the fourth and final section.1. Correct threading
In what situations might an empty
synchronized
block enable correct threading?Consider an example.
The code above is incorrect. The runtime might isolate thread A’s change to the boolean variable
toExit
, effectively hiding it from B, which would then loop forever.It can be corrected by introducing empty
synchronized
blocks, as follows.2. Basis for correctness
How do the empty
synchronized
blocks make the code correct?The Java memory model guarantees that an ‘unlock action on monitor m synchronizes-with all subsequent lock actions on m’ and thereby happens-before those actions (§17.4.4). So the unlock of monitor
o
at the tail of A’ssynchronized
block happens-before its eventual lock at the head of B’ssynchronized
block. And because A’s write to the variable precedes its unlock and B’s lock precedes its read, the guarantee extends to the write and read operations: write happens-before read.Now, ‘[if] one action happens-before another, then the first is visible to and ordered before the second’ (§17.4.5). It is this visibility guarantee that makes the code correct in terms of the memory model.
3. Comparative utility
How might an empty
synchronized
block be a better solution than the alternatives?Versus a non-empty `synchronized` block
One alternative is a non-empty
synchronized
block. A non-emptysynchronized
block does two things: a) it provides the ordering and visibility guarantee described in the previous section, effectively forcing the exposure of memory changes across all threads that synchronize on the same monitor; and b) it makes the code within the block effectively atomic among those threads; the execution of that code will not be interleaved with the execution of other block-synchronized code.An empty
synchronized
block does only (a) above. In situations where (a) alone is required and (b) could have significant costs, the emptysynchronized
block might be a better solution.Versus a `volatile` modifier
Another alternative is a
volatile
modifier attached to the declaration of a particular variable, thereby forcing exposure of its changes. An emptysynchronized
block differs in applying not to any particular variable, but to all of them. In situations where a wide range of variables have changes that need exposing, the emptysynchronized
block might be a better solution.Moreover a
volatile
modifier forces exposure of each separate write to the variable, exposing each across all threads. An emptysynchronized
block differs both in the timing of the exposure (only when the block executes) and in its extent (only to threads that synchronize on the same monitor). In situations where a narrower focus of timing and extent could have significant cost benefits, the emptysynchronized
block might be a better solution for that reason.4. Correctness revisited
Concurrent programming is difficult. So it should come as no surprise that empty
synchronized
blocks can be ‘subtle and hard to use correctly’. One way to misuse them (mentioned by Holger) is shown in the example below.Thread B’s statement “
if( toExit ) exit( exitValue )
” assumes a synchrony between the two variables that the code does not warrant. Suppose B happens to readtoExit
andexitValue
after they’re written by A, yet before the subsequent execution of bothsynchronized
statements (A’s then B’s). Then what B sees may be the updated value of the first variable (true
) together with the un-updated value of the second (zero), causing it to exit with the wrong value.One way to correct the code is through the mediation of final fields.
The revised code is correct because the Java memory model guarantees that, when B reads the new value of
state
written by A, it will see the fully initialized values of the final fieldstoExit
andexitValue
, both being implicitly final in the declaration ofState
. ‘A thread that can only see a reference to an object after that object has been completely initialized is guaranteed to see the correctly initialized values for that object's final fields.’ (§17.5)Crucial to the general utility of this technique (though irrelevant in the present example), the specification goes on to say: ‘It will also see versions of any object or array referenced by those final fields that are at least as up-to-date as the final fields are.’ So the guarantee of synchrony extends deeply into data structures.
Subtleties
Local caching of the
state
variable by thread B (example above) might seem unnecessary whenstate
is known to change once only. While it has its original value, the statement “if( state.toExit ) exit( state.exitValue )
” will short circuit and read it once only; otherwise it will have its final value and be guaranteed not to change between the two reads. But as Holger points out, there is no such guarantee.Consider what could happen if we leave the caching out.
‘An implementation is free to produce any code it likes, as long as all resulting executions of a program produce a result that can be predicted by the memory model. This provides a great deal of freedom for the implementor to perform a myriad of code transformations, including the reordering of actions and removal of unnecessary synchronization.’ (§17.4)
Seeing therefore that “
if( state.toExit ) exit( state.exitValue )
” lies outside of the synchronized block, and thatstate
is a non-volatile variable, the following transformation would be valid.This might actually be how the code executes. Then the first read of
state
(intos
) might yield its original value, while the next read yields its final value, causing the program to exit unexpectedly with a value of 0 instead of 1.过去的情况是,规范暗示发生了某些内存屏障操作。 然而,规范现在已经改变,原始规范从未正确实施。 它可用于等待另一个线程释放锁,但协调另一个线程已经获取锁会很棘手。
It used to be the case that the specification implied certain memory barrier operations occurred. However, the spec has now changed and the original spec was never implemented correctly. It may be used to wait for another thread to release the lock, but coordinating that the other thread has already acquired the lock would be tricky.
同步的作用不仅仅是等待,虽然不优雅的编码也可以达到所需的效果。
来自 http://www.javaperformancetuning.com/news/qotm030.shtml
Synchronizing does a little bit more than just waiting, while inelegant coding this could achieve the effect required.
From http://www.javaperformancetuning.com/news/qotm030.shtml
要深入了解 Java 的内存模型,请观看 Google 的“编程语言高级主题”系列中的视频:
http://www.youtube.com/watch?v=1FX4zco0ziY
它给出了关于编译器可以(通常在理论上,但有时在实践中)对代码执行的操作的非常好的概述。 对于任何认真的 Java 程序员来说都是必备的东西!
For an in depth look into Java's memory model, have a look at this video from Google's 'Advanced topics in programming languages' series:
http://www.youtube.com/watch?v=1FX4zco0ziY
It gives a really nice overview of what the compiler can (often in theory, but sometimes in practice) do to your code. Essential stuff for any serious Java programmer!