In what situations could an empty synchronized block achieve correct threading semantics?
Asked Answered
B

5

30

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?

Bruges answered 26/3, 2009 at 16:5 Comment(0)
V
20

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.

Verism answered 26/3, 2009 at 16:10 Comment(7)
side note: I would definitely replace the concept of empty synchronize block with one of the java.util.concurrent classes. Locks/Barries/Latches all work well for this and the meaning is always explicit from the use (as opposed to magical emtpy braces)Highup
the other important usage is that it acts like a memory barrier (like reading/writing a volatile variable), A @SnakE discusses below.Erlina
Exactly. I have a method that makes some threads act like workers and others as consumers. All consumers do is use empty synchronized to wait until worker is done modifying instance and since then - there is no need for further synchronization so all the reading is done outside synchronized code. I believe synchronized is way clearer solution that managing lock instances manually.Encroachment
@Pius, is there no possibility that another worker could modify the instance while you're reading it?Verism
@Paul Tomblin No. Worker is the first to synchronize the instance and once it releases it, no other thread modifies it. This is a very specific case which I haven't applied anywhere else yet.Encroachment
@Pius, so only one Worker? Or thing that is consumed is never written to after the first Worker has finished writing it, even if there are other Workers? No possibility that the first Worker could write to it again? That seems like a very specialized case - mostly I think you'd want to synchronize the writing and the reading, or better yet use java.util.concurrent.locks to grab a read only ReadWriteLock while reading and a write lock while writing.Verism
@PaulTomblin Yes, every situation requires different tools. I just chose such minimum model for that specific case I didn't want to block reading (which is done after instance is ready) since no modifications may be done to the instance. To help you image, it goes like this: there is a central code base that loads images from either URL or local cache. The thing is that I don't want to be downloading same image multiple times for different threads so I turn first to request thread into a worker and all the other threads which want the same image become the consumers. Fewer threads, simple codeEncroachment
H
19

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.

import static java.lang.System.exit;

class Example { // Incorrect, might never exit

    public static void main( String[] _args ) { new Example().enter(); }

    void enter() {
        new Thread( () -> { // A
            for( ;; ) {
                toExit = true; }})
            .start();
        new Thread( () -> { // B
            for( ;; ) {
                if( toExit ) exit( 0 ); }})
            .start(); }

    boolean toExit; }

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.

import static java.lang.System.exit;

class Example { // Corrected

    public static void main( String[] _args ) { new Example().enter(); }

    void enter() {
        new Thread( () -> { // A
            for( ;; ) {
                toExit = true;
                synchronized( o ) {} }}) // Force exposure of the change
            .start();
        new Thread( () -> { // B
            for( ;; ) {
                synchronized( o ) {} // Seek exposed changes
                if( toExit ) exit( 0 ); }})
            .start(); }

    static final Object o = new Object();

    boolean toExit; }

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’s synchronized block happens-before its eventual lock at the head of B’s synchronized 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-empty synchronized 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 empty synchronized 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 empty synchronized 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 empty synchronized 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 empty synchronized 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 empty synchronized 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.

import static java.lang.System.exit;

class Example { // Incorrect, might exit with a value of 0 instead of 1

    public static void main( String[] _args ) { new Example().enter(); }

    void enter() {
        new Thread( () -> { // A
            for( ;; ) {
                exitValue = 1;
                toExit = true;
                synchronized( o ) {} }})
            .start();
        new Thread( () -> { // B
            for( ;; ) {
                synchronized( o ) {}
                if( toExit ) exit( exitValue ); }})
            .start(); }

    static final Object o = new Object();

    int exitValue;

    boolean toExit; }

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 read toExit and exitValue after they’re written by A, yet before the subsequent execution of both synchronized 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.

import static java.lang.System.exit;

class Example { // Corrected

    public static void main( String[] _args ) { new Example().enter(); }

    void enter() {
        new Thread( () -> { // A
            for( ;; ) if( !state.toExit() ) {
                state = new State( /*toExit*/true, /*exitValue*/1 );
                synchronized( o ) {} }})
            .start();
        new Thread( () -> { // B
            for( ;; ) {
                synchronized( o ) {}
                State state = this.state; /* Local cache.  It might seem
                  unnecessary when `state` is known to change once only,
                  but see § Subtleties in the text. */
                if( state.toExit ) exit( state.exitValue ); }})
            .start(); }

    static final Object o = new Object();

    static record State( boolean toExit, int exitValue ) {}

    State state = new State( /*toExit*/false, /*exitValue*/0 ); }

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 fields toExit and exitValue, both being implicitly final in the declaration of State. ‘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 when state 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.

new Thread( () -> { // B
    for( ;; ) {
        synchronized( o ) {}
     // State state = this.state;
    //// not required when it’s known to change once only
        if( state.toExit ) exit( state.exitValue ); }})
    .start(); }

‘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 that state is a non-volatile variable, the following transformation would be valid.

new Thread( () -> { // B
    for( ;; ) {
        synchronized( o ) {}
     // State state = this.state;
    //// not required when it’s known to change once only
        State s = state;
        if( state.toExit ) exit( s.exitValue ); }})
    .start(); }

This might actually be how the code executes. Then the first read of state (into s) 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.

Hunt answered 11/8, 2015 at 4:31 Comment(9)
"the effect of the volatile modifier does not extend to the variable's contents" that's pretty confused language. I think what you mean to say is that two threads reading a volatile do not create a happens-before relationship. However, a write and a read (if the read successfully reads the write) does create such a relationship. A happens-before relationship extends into everything that was done by a thread.Hispanicism
Also, all modern processors are cache-coherent. The happens-before relationship is more about what the compiler is allowed to do rather than the CPU.Hispanicism
@Aleksandr, I corrected the answer - again - this time dropping the misleading ‘cache’ references completely.Hunt
@Holger The question of comparative utility is an interesting one. And whatever the rights and wrongs of my answer, I think it was poorly written. So I pared it down to a narrower focus on the question, and appended a section on comparative utility.Hunt
This is a big improvement. But there’s one thing to consider: it is still possible that the actions are executed in the following order: A: write, B: synchronized block, A: synchronized block, B: read. Then, there is no happens-before relationship but the written value may still be perceived by B through the racy read. This doesn’t matter for this specific example as it’s only a single atomic int variable (but then, there are plenty of alternatives, volatile, AtomicInteger, or VarHandle). But in any scenario involving more than one variable, this would be broken.Ina
@Holger, there’s no guarantee of synchrony among multiple variables, I agree. And it’s certainly one of the gotchas that can make the technique ‘subtle and hard to use correctly’, as the OP quotes. So I added a section to deal with it.Hunt
Beware. if( state.toExit ) exit( state.exitValue ); is not guaranteed to work here, because these are two reads of state without inter-thread semantics. Without correct synchronization, reads and writes can be perceived out of order, so it’s possible that the first read inside the if’s condition perceives a newer value than the read inside the exit(…) invocation statement. You must read the reference into a local variable first (And it’s again a case where the alternatives volatile, AtomicInteger, or VarHandle would work better than an empty synchronized block).Ina
And that’s just wrong. As said, you have a racy read and it’s not guaranteed that a second racy read will see the newer value when the first racy read saw the newer value. if( state.toExit ) may see a newer value of state, followed by exit( state.exitValue ); seeing the older value of state. To prevent out of order reads you would have to declare the variable as volatile but since the whole example is about avoiding that, you have to use a local variable.Ina
@Ina (a) I agree, the local cache of the state variable should not be commented out. I uncommented it in the code and explained why in the text. (b) I do not claim here that empty synchronized statements are ever necessarily better than the alternatives, only potentially so, and I give reasons. Anyone claiming the contrary (that they are never better) would themselves owe us reasons for thinking that.Hunt
B
5

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.

Bearcat answered 26/3, 2009 at 17:23 Comment(0)
T
4

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

  1. The thread acquires the lock on the monitor for object this (assuming the monitor is unlocked, otherwise the thread waits until the monitor is unlocked).
  2. The thread memory flushes all its variables, i.e. it has all of its variables effectively read from "main" memory (JVMs can use dirty sets to optimize this so that only "dirty" variables are flushed, but conceptually this is the same. See section 17.9 of the Java language specification).
  3. The code block is executed (in this case setting the return value to the current value of i3, which may have just been reset from "main" memory).
  4. (Any changes to variables would normally now be written out to "main" memory, but for geti3() we have no changes.)
  5. The thread releases the lock on the monitor for object this.
Timon answered 30/3, 2009 at 6:45 Comment(1)
This is a dangerous simplification of the true rules. A synchronized block doesn't "flush its variables to (global) memory". The only guarantee is that if thread A synchronizes on a particular object and then thread B synchronizes on the same object later, then thread B will see thread A's changes.Hypnotherapy
C
0

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!

Churchwell answered 26/3, 2009 at 18:58 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.