0

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'?

10
  • 1
    Why an empty synchronized block? Commented Sep 25, 2023 at 12:06
  • @MauricePerry If without the empty synchronized block, the code will always run and never exits. Commented Sep 25, 2023 at 12:07
  • It seams your goal is to protect i. so I you should put the test inside the synchronized block. Commented Sep 25, 2023 at 12:08
  • @MauricePerry I want to know why the empty synchronized make the code exits? Commented Sep 25, 2023 at 12:09
  • 1
    AFAIK it's a side-effect and not guaranteed. If i is not declared volatile then there is no guarantee that the observed value of i inside the loop ever changes. There are many situations where (with the current OpenJDK implementation) it will become visible, but those aren't guaranteed by the spec. Commented Sep 25, 2023 at 12:18

1 Answer 1

4

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.

Sign up to request clarification or add additional context in comments.

4 Comments

Great detailed answer as always. Only minor nit: I'd be a bit more explicit what "the answer is no" means, as that depends very much on the specific phrasing of the question and sometimes questions are unclear about which way around they phrase it (or use different phrasing throughout the question).
@JoachimSauer I've qualified my no a little bit, you know, by changing it to a 'yes' - between eyeballs and brain neurons the question flipped into 'is this code problematic?', but the actual question is: "Is it guaranteed to print 'END'". Which it is, for this very specific code, the way I understand the JMM, yes, that is a guarantee. For an encore I've added an addendum that delves into whether the empty-ness of the synchronized block is an issue. It isn't.
Thanks for the great answers, just one more question, what if we add one line inside the empty synchronized, it becomes to: synchronized (Visibility.class){ int x = i;}, now is it be guaranteed by the JVM that it will always print 'END'?
@Anonemous I edited the answer in a rather crucial way - it was guaranteed to print END always. With 'no', I meant: "No, this code doesn't have JMM problems".

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.