Skip to the content.
blog talks comics about

Java's synchronized explained

There’s a ton of documentation and specifications describing the internal workings of the JVM. However, going through all of that to simply gain some understanding might be a bit too much. I’ve read those specs and looked inside, so here’s a simple little explanation of how synchronized works in Java.

Java syntax allows for synchronized methods and synchroized blocks (in docs, they call them “statements”). The idea is that only one thread can execute them at a time, so we can avoid competition for resources etc. etc.

monitors

Synchronized methods and statements look slightly different under the hood.

Here’s an example of a synchronized statement:

final Object a = new Object();

public void normalMethod() {
    System.out.println("outside synchronized block");

    synchronized (a) {
        System.out.println("inside synchronized block");
    }
}

Here’s the bytecode of the normalMethod:

 0: getstatic     #23    // Field java/lang/System.out:Ljava/io/PrintStream;
 3: ldc           #39    // String normal method outside synchronized block
 5: invokevirtual #31    // Method java/io/PrintStream.println:(Ljava/lang/String;)V
 8: aload_0
 9: getfield      #7     // Field a:Ljava/lang/Object;
12: dup
13: astore_1
14: monitorenter
15: getstatic     #23    // Field java/lang/System.out:Ljava/io/PrintStream;
18: ldc           #41    // String normal method inside synchronized block
20: invokevirtual #31    // Method java/io/PrintStream.println:(Ljava/lang/String;)V
23: aload_1
24: monitorexit
25: goto          33
28: astore_2
29: aload_1
30: monitorexit
31: aload_2
32: athrow
33: return

The most interesting part begins when we encounter synchronized in Java code, which, in the bytecode, starts with the monitorenter instruction. Before that instruction, the compiler made sure that a reference to our Object a is present on top of the operand stack, so monitorenter could use it and lock its monitor.

What is a monitor? You can think of it as some metadata that every instance has. Every object has a “monitor” associated with it, and when a thread locks it, other threads must wait until it’s released. It’s possible to see whether that object’s monitor is locked by simply checking its header. The object knows: who is the owner, and how many times that owner has obtained that same lock (reentered). Eventually, the thread must let go of the monitor exactly as many times as it has acquired it.

In the example above, we use a dummy Object to control that only one thread executes the piece of code inside synchronized. Alternatively, we can synchronize on a specific instance that we don’t want to access concurrently. But the rule is, there always must be some object.

Further in the bytecode, after we’re done with our single-threaded piece of logic, we encounter a monitorexit instruction. If monitorenter uses the reference and increments its counter of locks by this thread, the monitorexit works similarly - it uses the reference and decrements the counter. If a thread tries to release a monitor it does not own, an IllegalMonitorStateException is thrown.

Did you notice? The monitorexit instruction appears twice! Well, while we’re executing some code inside a synchronized block, we might encounter an exception, which would lead to us missing a monitorexit instruction. But because it’s very important to let go of that monitor in any case, the compiler inserted a piece of logic identical to a finally block:

Exception table:
 from    to  target type
    15    25    28   any
    28    31    28   any

If things don’t go well while we’re in a synchronized block (lines 15 - 25), we jump to instruction at line 28, and proceed with the following set of instructions:

28: astore_2
29: aload_1
30: monitorexit
31: aload_2
32: athrow
33: return

Basically: save a reference pointing to the exception, put the reference used for synchronized onto the stack, use it to execute monitorexit, take that exception reference again and throw it using athrow. Done!

Also, if something goes wrong between lines 28 and 31 (the “finally” block), we’ll end up in that same block again.

synchronized methods

As I mentioned earlier, synchronized methods look a bit different. They don’t have explicit monitorenter or monitorexit instructions, but internally, the JVM still uses monitors.

virtual

Let’s consider an example of a virtual synchronized method:

synchronized public void virtualSyncMethod() {
    System.out.println("virtual synchronized method");
}

Here’s the bytecode:

public synchronized void virtualSyncMethod();
descriptor: ()V
flags: (0x0021) ACC_PUBLIC, ACC_SYNCHRONIZED
Code:
  stack=2, locals=1, args_size=1
     0: getstatic     #23                 // Field java/lang/System.out:Ljava/io/PrintStream;
     3: ldc           #37                 // String virtual synchronized method
     5: invokevirtual #31                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
     8: return
  LineNumberTable:
    line 18: 0
    line 19: 8
  LocalVariableTable:
    Start  Length  Slot  Name   Signature
        0       9     0  this   Lorg/example/SyncMethods;

The JVM knows that a method is synchronized based on the ACC_SYNCHRONIZED flag. Even though we don’t see any monitor-something instructions, internally, the JVM still needs an object to coordinate the locking. When the method is called, some monitor must be locked, and on return or athrow instructions, the lock must be released. The solution is simple: JVM uses this reference, which is the reference to the owner instance. This means that if a class contains multiple synchronized methods, and one of these methods is in use, other threads cannot access any other synchronized methods of the same instance. Different instance - no problem.

In our example, in the LocalVariableTable, we can see a this reference, which points to an instance of the class containing my demo methods, SyncMethods. This reference will come in handy for the JVM.

static

Let’s take a quick look at the same situation, but with a static method:

synchronized public static void staticSyncMethod() {
    System.out.println("static synchronized method");
}

The bytecode:

public static synchronized void staticSyncMethod();
descriptor: ()V
flags: (0x0029) ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED
Code:
  stack=2, locals=0, args_size=0
     0: getstatic     #23                 // Field java/lang/System.out:Ljava/io/PrintStream;
     3: ldc           #29                 // String static synchronized method
     5: invokevirtual #31                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
     8: return
  LineNumberTable:
    line 14: 0
    line 15: 8

Looks very similar, but of course, it can’t have any kind of this reference because there is no owning instance for this method. In this case, the JVM uses a reference to the whole class containing the method (for example, SyncMethods.class). That is why, across all instances, two synchronized methods of the SyncMethods class can’t be executed concurrently by different threads.


Of course, there are more details about synchronization in the JVM that I didn’t cover, and that was done intentionally to keep it digestible. If you have any specific feedback or would like to read about a particular topic in the future, feel free to contact me on Bluesky.

Thanks and see you soon!

References:
monitorenter/monitorexit specs
Intrinsic Locks and Synchronization
Synchronization
Threads and Locks

back to blog
Hits