In what situations could an empty synchronized block achieve correct threading semantics?
An empty synchronized block will wait until nobody else is using that synchronizer. 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.
I think the earlier answers fail to underline the most useful thing about empty synchronized
blocks: exposing variable changes and other actions across threads. As jtahlborn indicates, synchronization does this by imposing a memory barrier on the compiler. I didn’t find where SnakE is supposed to have discussed this, though, so here I explain what I mean.
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();
}
The code above is incorrect. The compiler might isolate thread A’s change to the variable, effectively hiding it from B, which would then loop forever.
Using empty synchronized
blocks to expose a change across threads
One correction is to add a volatile
modifier to the variable. But this can be inefficient; it forces the compiler to expose all changes, which might include intermediate values of no interest. Empty synchronized
blocks, on the other hand, only expose the changed value at critical points. For example:
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();
How the memory model guarantees visibility
Both threads must synchronize on the same object to guarantee visibility. The guarantee rests on the Java memory model, in particular on the rule that an “unlock action on monitor m synchronizes-with all subsequent lock actions on m” and thereby happens-before those actions. So the unlock of o’s monitor at the tail of A’s synchronized
block happens-before the eventual lock at the head of B’s block. And because A’s write precedes its unlock and B’s lock precedes its read, the guarantee extends to cover both write and read — write happens-before read — making the revised program correct in terms of the memory model.
I think this is the most important use for empty synchronized
blocks.
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.