Search code examples
javamultithreadingsynchronizedmemory-barriersmemory-visibility

Java memory visibility outside the synchronized lock


Does the synchronized lock guarantee the following code always print 'END'?

public class Visibility {

    private static int i = 0;

    public static void main(String[] args) throws InterruptedException {

        Thread thread_1 = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(i);
                while (true) {
                    if (i == 1) {
                        System.out.println("END");
                        return;
                    }
                    synchronized (Visibility.class){}
                }
            }
        });
        thread_1.start();
        Thread.sleep(1000);
        synchronized (Visibility.class) {
            i = 1;
        }
    }
}

I run it on my laptop, it always print 'END', but I am wondering is it be guaranteed by the JVM that this code will always print 'END'?

Further, If we add one line inside the empty synchronized block, and it becomes to:

public class Visibility {

    private static int i = 0;

    public static void main(String[] args) throws InterruptedException {

        Thread thread_1 = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(i);
                while (true) {
                    if (i == 1) {
                        System.out.println("END");
                        return;
                    }
                    synchronized (Visibility.class){
                        int x = i; // Added this line.
                    }
                }
            }
        });
        thread_1.start();
        Thread.sleep(1000);
        synchronized (Visibility.class) {
            i = 1;
        }
    }
}

Now is it be guaranteed by the JVM that this code will always print 'END'?


Solution

  • As per JLS §17.4.5: Happens-before order:

    If hb(x, y) and hb(y, z), then hb(x, z).

    In other words, HB (Happens-before) is transitive. And HB is the primary player in observability: It is not possible to observe state in line Y as it was before line X executed if hb(x, y) - and that's exactly what you are trying to do (or rather, prevent from happening): You are interested in whether line Y (if (i == 1)) can observe state as it was prior to line X (i = 1, in the synchronized block at the bottom of the snippet).

    Given that transitivity rule, the answer for your specific snippet is 'yes' - you're guaranteed that it prints END. Always take care in extrapolating an analysis for one particular snippet to a more general case - this stuff doesn't simplify easily, you have to apply happens-before analysis every time (or, more usually, avoid interactions between threads by way of field writes as much as you can):

    • hb(exiting a synchronized block, entering it) is a thing. different threads acquiring the monitor is an ordered affair, and there are HB relationships between these. Hence, there's an hb relationship between the second-to-last } (exiting the block) 1 and the zero-statements synchronized block in the thread.

    • If the inner thread somehow runs afterwards (weird, as that would mean it has taken more than 1 second to even start, but technically a JVM makes no timing guarantees, so possible in theory), i is already 1. It's possible this change has not been 'synced' yet, except then the while loop hits that no-content synchronized block which then established hb, and thus forces visibility of i being 1.

    • If the inner thread runs before (100% of all cases, what with that Thread.sleep(1000) in there, pretty much), the same logic applies eventually.

    • To actually add it together, we need to add the 'natural' hb rule: Bytecode X and Y establish hb(x, y) if X and Y are executed by the same thread and Y comes after X in program order. i.e. given: y = 5; System.out.println(y) you can't observe y being as it was before y = 5; ran - this is the 'duh!' HB rule - java would be rather useless as a language if the JVM can just reorder stuff in a single thread at will, of course. That + the transitivity rule is enough.

    One technical issue here

    The thread you fire up never voluntarily relinquishes which can cause some havoc (nothing in that code will 'sleep' the CPU). You should never write code that just busy-spins like this, the JVM is not particularly clear about how things work then, and it'll cause massive heat/power issues on the CPU! On a single core processor, the JVM is essentially allowed to spend all its time on that busy-wait, forever, and this is the one way in which you can make a JVM not print END, ever: Because the main thread that sets i to 1 never gets around to it. This breaks the general point of threads. synchronized introduces a savepoint, so it'll eventually get pre-empted, but that can take quite a while. It can take far longer than a second on the right hardware.

    Trivially fixed by shoving some sort of sleep or wait in that busy-loop, or using j.u.concurrent locks/semaphores/etc.

    But won't that empty synchronized block get optimized out?

    No. The JLS dictates pretty much down to the byte exactly what javac must produce. It is not allowed to optimize empty loops. It's the hotspot engine (i.e. java.exe - the runtime) that does things like 'oh, this loop has no observable effects that I'm supposed to guarantee, therefore, I can optimize the whole thing away entirely', and the JMM 17.4.5 indicates it cannot do that. If a JVM impl 'optimizes it away', it'd be buggy.

    We can confirm this with javap:

    > cat Test.java
    class Test {
      void test() {
        synchronized(this) {}
      }
    }
    > javac Test.java; javap -c -v Test
    [ ... loads of output elided ...]
    3: monitorenter
    4: aload_1
    5: monitorexit
    

    monitorenter is bytecode for the opening brace in a synchronized (x){} block, and monitorexit is bytecode for the closing brace (or any other control flow out of one - exception throwing and break / continue / return also make javac emit monitorexit bytecode.

    Closing note

    I assume the question was asked in the spirit of: Soo.. what happens here? Not in the spirit of: "Is this fine to write". It's not okay - other than the spinloop (never good), forcing the reader to go on a goose chase determining that HB is in fact set up to ensure this code does what you think it does, and requiring multiple details about the HB ruleset (the synchronized stuff and the transitivity rule and knowledge that empty sync blocks aren't optimized away) - not to mention an obvious bit of bizarro code (an empty sync block) which nevertheless does have a function here, none of that is particularly maintainable. The proper way to do this specific job is most likely to use a simple lock from java.util.concurrent, or at least to move the synchronized block to encompass all content in the while block. Also, locking on things code outside your direct control can acquire (and Visibility.class is, trivially, a global singleton) is a very bad idea: The only non-broken way to do that, is to extensively document your locking behaviour (and therefore, you're now signed up to maintain that behaviour indefinitely, or you're forced to release a major (i.e. backwards incompatible) version if you change it). Almost always you want to lock on things you control - i.e. private final Object lock = new Object[0]; - an object that cannot possibly be referred to outside your direct control.


    [1] Technically HB is a JVM affair and applies to bytecode. That closing brace, however, really does have a bytecode equivalent (most closing braces do not; that one does): It's the release of the monitor ('freeing' the synchronized lock). Which is exactly the bytecode that is HB relative to a later-ordered acquiring of that same lock object.