21 How to See the Monitor Lock Behind Synchronized

21 How to See the Monitor Lock Behind synchronized #

In this lesson, we will study the monitor lock behind synchronized.

Timing of acquiring and releasing the monitor lock #

We all know that the simplest way to synchronize is to use the synchronized keyword to modify a code block or a method. In this protected code, at most one thread can run at the same time, and this is achieved by using the monitor lock behind synchronized. So first let’s look at the timing of acquiring and releasing the monitor lock. Each Java object can be used as a lock for synchronization. This lock is also called an intrinsic lock or monitor lock. The only way to obtain the monitor lock is to enter the synchronized code block or synchronized method protected by this lock. Before a thread enters a code block protected by synchronized, it automatically acquires the lock, and whether it exits through a normal path or by throwing an exception, it automatically releases the lock upon exit.

First, let’s look at an example of a method that is synchronized:

public synchronized void method() {

    method body

}

We can see that the method() method is synchronized. To better understand the underlying principle, let’s rewrite the above code into the following equivalent pseudocode:

public void method() {

    this.intrinsicLock.lock();

    try{

        method body

    }

    finally {

        this.intrinsicLock.unlock();

    }

}

In this writing, as soon as we enter the method() method, we immediately acquire the intrinsic lock, and use the try code block to protect the method, and finally release the lock using the finally block. Here, intrinsicLock is the monitor lock. After expanding the pseudocode in this way, I believe your understanding of synchronized will be clearer.

Use the javap command to view the disassembled result #

The JVM implements the details of synchronized methods and synchronized code blocks differently. Now let’s look at the implementation of the two separately.

Synchronized code block #

First, let’s look at the implementation of a synchronized code block, as shown in the code:

public class SynTest {

    public void synBlock() {

        synchronized (this) {

            System.out.println("lagou");

        }

    }

}

In the synBlock method of the SynTest class, there is a synchronized code block, and the synchronized code block contains a line of code that prints the string “lagou”. Now let’s see what the synchronized keyword really does using the following command: first use the cd command to switch to the directory where the SynTest.java class is located, then run javac SynTest.java, and a bytecode file named SynTest.class will be generated. Then we execute javap -verbose SynTest.class, and we will see the corresponding disassembled content.

Key information is as follows:

  public void synBlock();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=1
         0: aload_0
         1: dup
2: astore_1
3: monitorenter
4: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
7: ldc           #3                      // String lagou
9: invokevirtual #4               // Method java/io/PrintStream.println:(Ljava/lang/String;)V
12: aload_1
13: monitorexit
14: goto          22
17: astore_2
18: aload_1
19: monitorexit
20: aload_2
21: athrow
22: return

From the code, we can see that the synchronized block actually has two instructions: monitorenter and monitorexit. The highlighted instructions on lines 3, 13, and 19 correspond to the monitorenter and monitorexit instructions respectively. The reason for one monitorenter and two monitorexit instructions is that the JVM ensures that every monitorenter must have a corresponding monitorexit. The monitorenter instruction is inserted at the beginning of the synchronized block, while monitorexit needs to be inserted at the normal end and the exception end of the method in order to ensure that the lock is released even in case of exceptions.

We can interpret the execution of monitorenter as locking and the execution of monitorexit as unlocking. Each object maintains a counter that records the number of times it has been locked. An object that is not locked has a counter value of 0. Let’s take a closer look at the meaning of monitorenter and monitorexit:

  • monitorenter: The thread executing monitorenter attempts to acquire ownership of the monitor. There are three possible situations: a. If the counter for the monitor is 0, the thread acquires the monitor and sets its counter to 1. The thread becomes the owner of the monitor. b. If the thread already owns the monitor, it re-enters and increments the counter. c. If another thread already owns the monitor, the current thread is blocked until the counter for the monitor becomes 0, indicating that the monitor has been released. The current thread then tries to acquire the monitor again.

  • monitorexit: The purpose of monitorexit is to decrement the counter for the monitor until it reaches 0. This indicates that the monitor has been released and no thread owns it. It represents the unlock operation. Therefore, other threads waiting for the monitor can now try to acquire ownership of it again.

Synchronized Methods #

From the above, we can see that a synchronized block is implemented using monitorenter and monitorexit instructions. On the other hand, synchronized methods are not implemented using monitorenter and monitorexit instructions. When decompiled with javap, synchronized methods appear very similar to ordinary methods, with the difference being that they have a flag modifier called ACC_SYNCHRONIZED to indicate that they are synchronized methods.

Here is an example of a synchronized method:

public synchronized void synMethod() {

}

The corresponding decompiled instructions are as follows:

public synchronized void synMethod();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED

    Code:
      stack=0, locals=1, args_size=1
         0: return
      LineNumberTable:
        line 16: 0

As we can see, synchronized methods have an ACC_SYNCHRONIZED flag. When a thread wants to access a method, it first checks if the method has the ACC_SYNCHRONIZED flag. If it does, the thread needs to acquire the monitor lock before it can start executing the method. After the method is executed, the monitor lock is released. In other aspects, synchronized methods are very similar to synchronized blocks. For example, if another thread tries to execute the method while the monitor lock is held, it will be blocked because it cannot acquire the monitor lock.

That’s it for this lesson. In this lesson, we discussed the timing of acquiring and releasing monitors, equivalent code for synchronized methods, and used the javac and javap commands to inspect the disassembled instructions for synchronized blocks and synchronized methods, where synchronized blocks are implemented using monitorenter and monitorexit instructions, while synchronized methods are implemented using flags.