1

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; /* ? */
    }
TwITe
  • 400
  • 2
  • 14
  • Why not just use a `ReentrantLock` instead? one thread locks (or waits to lock), writes, unlocks, the other one locks (or waits to lock), reads, unlocks ? Sure, there is some ovearhead in the threads, in the form of waiting for the lock to free-up but whats the real issue? ... Other than ReaderThread obtaining the lock first and seeing no new values; if these are supposed to be a lock-step kind of thing then simply `synchronized` won't help there. – Shark Aug 05 '22 at 15:02
  • @Shark it's a barely practical question but more theoretical. I'm just trying to understand jmm better. So let's stick with these basic synchronization primitives – TwITe Aug 05 '22 at 15:05
  • 2
    "I've put `sync` variable to forbid the compiler/JVM to remove otherwise empty `synchronized` block" I don't think an empty synchronized block can be optimized away, because of the change to the memory model semantics. – Andy Turner Aug 05 '22 at 15:05
  • ok, then to answer your question - due to the `synchronized` writer block, `a` will most certainly be 5 due to the happens-before. however, since the code is procedural, `b` will either be 0 (due to it not being initialized to anything) or 7, and i'm gonna guess it's most likely going to be 7, because it's preceded by a `synchronized` block. If you however first read `b` then read `a` in a `synchronized` block, then `b` will be either 0 or 7, no guarantees, due to an implicit and obvious data-race. – Shark Aug 05 '22 at 15:41
  • However, since there is no code that illustrates or demonstrates how the `reader()` and `writer()` code is being called (with different threads or not), i can't really give you a unified answer that covers both examples, since in the first sample, `a` is specifically non-volatile, so it's prone to LTS (local thread caching) and it's going to be different for all threads unless made `volatile`. The second sample doesn't specify what `a` and `b` are, and we can only guess that they share the same declaration as in the first block. – Shark Aug 05 '22 at 15:44
  • Either way, i'd either suggest usage of a doubly-locked block (doubly checked locking is what you wanna read on, or [this answer](https://stackoverflow.com/questions/1625118/java-double-checked-locking#1625180)), or think about your usecase, and redesign it to not rely on just JVM but actual real `Lock`s. And you'll most likely want to use a `ReentrantLock` in that case, because it' can be a shared, reusable lock. – Shark Aug 05 '22 at 15:47
  • And i'm not even going to touch the fact that "double-checked locking is an antipattern" with a 10 foot stick here, because it's outside of the scope of your question or this "purely rhetorical/theoretical question". I personally don't think it's an antipattern or consider it one, but if it really is - it's not a horrible one, the benefits outweight it's cost. – Shark Aug 05 '22 at 15:51
  • ... and then he kicks the "fact" hard. Nah ... not biting :-) – Stephen C Aug 06 '22 at 00:05

4 Answers4

2

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;
}
Eugene
  • 117,005
  • 15
  • 201
  • 306
  • You still need to ensure that you read a/b only after you enter that side of the branch. Otherwise you still have a data race. So if you would do 'if(sync==0)return;' that would introduce the hb-edge. But the example is still very artificial. – pveentjer Aug 16 '22 at 08:17
  • Oh, yes. This is my mistake, thank you for fixing and clarifying that. – TwITe Aug 16 '22 at 10:54
1

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.

1

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.

Malt
  • 28,965
  • 9
  • 65
  • 105
  • 1
    The happens-before edge between 3 and 4 only exists if 4 observes the 'sync' value written by 3. Since nothing is done with the read 'sync' value, we do not know if the value was observed and therefore no happens-before edge exists for all executions and hence there is data-race. To solve this problem you need to do something with the read 'sync' value that would prevent continuing if the 'sync' value was not set. – pveentjer Aug 06 '22 at 13:55
  • 1
    It becomes even more interesting if 0 would be written to sync instead of 1. Because then the synchronized blocks might have no semantics within the JMM at all. – pveentjer Aug 06 '22 at 13:58
  • @pveentjer isn't this called happens-before consistency? I mean you are correct that `(4)` has to observe `(3)` in order for the proper edge to be created, but the name for this rule would be "happens-before consistency" – Eugene Aug 16 '22 at 07:48
  • There is no 'rule' happens-before consistency that is comparable to e.g. program order rule, or volatile variable rule, or monitor lock rule. Happens before consistency means that either a read the most recent write before it in the happens-before order, or a write it is in data-race with. – pveentjer Aug 16 '22 at 07:59
  • But yes; the code above has a data race and hence it needs to conform to the definition/rule of happens-before consistent. – pveentjer Aug 16 '22 at 08:02
1

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 answers.

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.

Stephen C
  • 698,415
  • 94
  • 811
  • 1,216
  • Isn't it allowed to read without mutual exclusion if there's only single writing thread at any given time? – TwITe Aug 06 '22 at 13:37
  • 2
    @TwITe: no. Because then you would not have a happens-before edge between the write and the read and weird things can happen like the compiler optimizing out the load. If you want to have a super cheap read that isn't optimized out and is atomic, you could have a look at the VarHande getOpaque. But keep in mind that technically it is still a data race and you really need to know what you are doing. – pveentjer Aug 06 '22 at 16:49
  • Also, it depends on what you mean by "allowed". The compiler will allow you to write code that isn't thread-safe. – Stephen C Aug 06 '22 at 23:16
  • 3
    @pveentjer but keep in mind that `getOpaque` does not provide the transitivity (that’s why it is cheap). Especially important in a question about piggybacking… – Holger Sep 07 '22 at 07:10
  • @Holger correct. It doesn't provide happens-before edges and therefor transitivity doesn't apply. – pveentjer Sep 07 '22 at 07:24