简体   繁体   中英

Visibility of a read after synchronized block

Does JMM guarantee the visibility of a synchronized write to the variable that is read in the other thread after a synchronized block? Here's what I mean:

public class SynchronizedWriteRead {

    private int a;
    private int b;

    public void writer() {
        synchronized (this) {
            a = 5;
            b = 7;
        }
    }

    public void reader() {
        synchronized (this) {
            int r1 = a; /* 5 */
        }
        int r2 = b; /* ? */
    }
}

JMM guarantees that an unlock on a monitor happens-before every subsequent lock on that monitor. But I'm not sure if it relates only to the synchronized block body or not.

Recently I'm encountered this post from Aleksey Shipilëv - Safe Publication and Safe Initialization in Java . It says:

Notice how doing synchronized in Unsafe DCL store does not help, contrary to layman belief it somehow magically "flushes the caches" or whatnot. Without a paired lock when reading the protected state, you are not guaranteed to see the writes preceding the lock-protected write.

So this is why I asked myself this question. I couldn't find an answer in the JLS .

Let's put it another way. Sometimes you're piggybacking on a volatile happens-before guarantee like this:

public class VolatileHappensBefore {

    private int a; /* specifically non-volatile */
    private volatile int b;

    public void writer() {
        a = 5;
        b = 7;
    }

    public void reader() {
        int r1 = b; /* 7 */
        int r2 = a; /* 5 */
    }
}

You're guaranteed to see both writes because sequential actions in the same thread are backed by happens-before, and happens-before itself is transitive.

Can I use a synchronized happens-before guarantee the same way? Maybe even like this (I've put sync variable to forbid the compiler/JVM to remove otherwise empty synchronized block):

    public void writer() {
        a = 5;
        b = 7;
        synchronized (this) {
            sync = 1;
        }
    }

    public void reader() {
        synchronized (this) {
            int r = sync;
        }
        int r1 = a; /* ? */
        int r2 = b; /* ? */
    }

You got your answer by now, and to be fair, everyone here is correct, I just want to add one important rule. It's called "happens-before consistency", which goes like this:

  • reads either see the latest write in happens-before order, or any other write not tied in happens-before order, thus a data-race.

So while the accepted answer is indeed correct, it should mention that in order for a happens-before edge to be created between (3) and (4) , (4) has to observe the write that (3) did.

In your example:

public void reader() {
   synchronized (this) {
      int r = sync;
   }
   int r1 = a; /* ? */
   int r2 = b; /* ? */

}

it means that int r = sync; is not correct, you need to assert that you actually saw sync to be 1 (you have observed the write). For example, this would create the needed edge:

if (sync == 1) {
    // guaranteed that r1=5 and r2=7
    int r1 = a;
    int r2 = b;
}

Does JMM guarantee the visibility of a synchronized write to the variable that is read in the other thread after a synchronized block?

Can I use a synchronized happens-before guarantee the same way?

Yes and yes.

synchronized and volatile give the same visibility guarantees.

In fact, a volatile variable behaves as if each read and write of this variable happens in its own tiny synchronized block.

In the JLS terms:

  • the visibility is guaranteed by the happens-before relation:

    Informally, a read r is allowed to see the result of a write w if there is no happens-before ordering to prevent that read.

  • volatile and synchronized are some of the ways to establish the happens-before relation:
    • An unlock on a monitor happens-before every subsequent lock on that monitor.
    • A write to a volatile field (§8.3.1.4) happens-before every subsequent read of that field.
    • ...

The quote from Safe Publication and Safe Initialization in Java describes the case when:

  • an object is initialized in a synchronized block in one thread
  • the object and is read without synchronized blocks in another thread.

In this situation the reader thread might see the object in the middle of initialization.

What's important to note here is that happens-before is a transitive relation. So if A happens-before B and B happens-before C , we can safely conclude that A happens-before C .

Now let's look at the code in question (I've added comments for clarity):

public void writer() {
    a = 5; //1
    b = 7; //2
    synchronized (this) { 
        sync = 1;
    } //3
}

public void reader() {
    synchronized (this) { //4
        int r = sync;
    }
    int r1 = a; //5 
    int r2 = b; //6
}

We know that 1 happens-before 2 and 2 before 3 since they're executed by the same thread:

If x and y are actions of the same thread and x comes before y in program order, then hb(x, y).

We also know that 3 happens before 4 :

An unlock action on monitor m synchronizes-with all subsequent lock actions on m (where "subsequent" is defined according to the synchronization order).

If an action x synchronizes-with a following action y, then we also have hb(x, y).

Lastly, we know that 4 happens-before 5 and 5 before 6 since they're executed in the same thread.

If x and y are actions of the same thread and x comes before y in program order, then hb(x, y).

So we end up with a chain of happens-before relationships from 1 to 6. Consequently, 1 happens-before 6 .

Does JMM guarantee the visibility of a synchronized write to the variable that is read in the other thread after a synchronized block?

Yes, for the reasons given in the other answer.

But there is a catch.

Let us assume that the reader call follows the writer call so that there is a happens before between the writer exiting its synchronized block and reader entering its block... and transitively it exiting the block and reading b .

The catch is when reader exits its block, there is no longer a guarantee of mutual exclusion on b . So suppose that another thread immediately acquires the mutex and modifies b . Since there is no HB chain connecting that write to b with the read in reader , there are no guarantees what the reader code will actually see that update... or the value that writer wrote previously.

In short, you need to consider more than HB relationships when reasoning about concurrent algorithms.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM