Do you know the underlying principle of the synchronized keyword?

Synchronized [Object Lock] uses a mutually exclusive method so that at most one thread can hold the [Object Lock] at the same time. Other threads will be blocked when they want to acquire this [Object Lock].

The code for grabbing tickets is as follows. If it is not locked, it will be oversold or one ticket will be sold to multiple people.

public class TicketDemo {<!-- -->
    static Object lock = new Object();
    int ticketNum = 10;
    public synchronized void getTicket() {<!-- -->
        synchronized (this) {<!-- -->
            if (ticketNum <= 0) {<!-- -->
                return;
            }
            System.out.println(Thread.currentThread().getName() + "Grab a ticket, remaining:" + ticketNum);
            // non-atomic operation
            ticketNum--;
        }
    }

    public static void main(String[] args) {<!-- -->
        TicketDemo ticketDemo = new TicketDemo();
        for (int i = 0; i < 20; i + + ) {<!-- -->
            new Thread(() -> {<!-- -->
                ticketDemo.getTicket();
            }).start();
        }
    }
}

Monitor

The bottom layer of Synchronized is actually a Monitor. Monitor is translated as monitor, which is provided by jvm and implemented in C++ language.

If you want to reflect the monitor in the code, you need to use the javap command to view the bytecode of clsss, such as the following code:

public class SyncTest {<!-- -->

    static final Object lock = new Object();
    static int counter = 0;
    public static void main(String[] args) {<!-- -->
        synchronized (lock) {<!-- -->
            counter + + ;
        }
    }
}

Find the class file of this class and execute javap -v SyncTest.class in the class file directory. The decompilation effect is as follows:

image-20230504165342501

  • monitorenter where locking begins
  • where monitorexit is unlocked
  • The instructions surrounded by monitorenter and monitorexit are the locked codes.
  • There are two reasons for monitorexit. The second monitorexit is to prevent the locked code from being able to release the lock in time after throwing an exception.

When using a synchornized code block, you need to specify an object, so synchornized is also called an object lock.

The monitor is mainly associated with this object, as shown below

image-20230504165833809

The specific storage structure inside Monitor:

  • Owner: stores the thread currently acquiring the lock. Only one thread can acquire it.

  • EntryList: associated threads that have not grabbed the lock and threads in the Blocked state

  • WaitSet: Associated with the thread that called the wait method, the thread in the Waiting state

Specific process:

  • The code enters the synchorized code block, first lets the monitor associated with the lock (object lock), and then determines whether the Owner has a thread holding it
  • If no thread holds it, let the current thread hold it, indicating that the thread successfully acquired the lock.
  • If there is a thread holding it, let the current thread enter the entryList to block. If the thread held by the Owner has released the lock, the threads in the EntryList compete for the lock ownership (unfair)
  • If the wait() method is called in the code block, it will wait in the WaitSet.

Reference answer:

  • Synchronized [Object Lock] uses a mutually exclusive method so that at most one thread can hold [Object Lock] at the same time

  • Its bottom layer is implemented by monitor, which is a jvm-level object (implemented in C++). To obtain a lock, a thread needs to use an object (lock) to associate the monitor.

  • There are three attributes inside the monitor, namely owner, entrylist, and waitset.

  • The owner is the associated thread that acquires the lock, and can only be associated with one thread; the entrylist is associated with the thread in the blocked state; the waitset is associated with the thread in the Waiting state.

The locks implemented by Monitor are heavyweight locks. Do you know about lock upgrades?

  • The lock implemented by Monitor is a heavyweight lock, which involves switching between user mode and kernel mode, and process context switching. The cost is high and the performance is relatively low.

  • Two new lock mechanisms were introduced in JDK 1.6: biased locks and lightweight locks. They were introduced to solve the performance overhead caused by the use of traditional lock mechanisms in scenarios where there is no multi-thread competition or basically no competition.

1. Object memory structure

In the HotSpot virtual machine, the layout of objects stored in memory can be divided into three areas: object header (Header), instance data (Instance Data) and alignment filling.

image-20230504172253826

We need to focus on analyzing the MarkWord object header

2. MarkWord

image-20230504172541922

  • Hashcode: 25-digit object identification Hash code

  • age: The generational age of the object occupies 4 digits

  • biased_lock: biased lock identification, occupies 1 bit, 0 means biased lock has not been started, 1 means biased lock is turned on

  • thread: The thread ID holding the biased lock, accounting for 23 bits

  • epoch: bias timestamp, occupies 2 bits

  • ptr_to_lock_record: In lightweight lock state, pointer to the lock record in the stack, occupies 30 bits

  • ptr_to_heavyweight_monitor: In the heavyweight lock state, the pointer to the object monitor Monitor, occupies 30 bits

We can determine the level of the lock through the lock’s logo.

  • The last three digits are 001 which means no lock
  • The last three digits are 101 which means bias lock
  • The last two digits are 00 indicating lightweight lock
  • The last two digits are 10, indicating a heavyweight lock
3. Let’s talk about Monitor heavyweight lock

Each Java object can be associated with a Monitor object. If you use synchronized to lock the object (heavyweight), the pointer to the Monitor object will be set in the Mark Word of the object header

image-20230504172957271

To put it simply: the object header of each object can set the monitor pointer to associate the object with the monitor.

4. Lightweight lock

In many cases, when a Java program is running, the code in the synchronized block does not compete, and different threads alternately execute the code in the synchronized block. In this case, heavyweight locks are not necessary. Therefore, JVM introduced the concept of lightweight locks.

static final Object obj = new Object();

public static void method1() {<!-- -->
    synchronized (obj) {<!-- -->
        // sync block A
        method2();
    }
}

public static void method2() {<!-- -->
    synchronized (obj) {<!-- -->
        // sync block B
    }
}

Lock process

1. Create a Lock Record in the thread stack and point its obj field to the lock object.

image-20230504173520412

2. Store the address of the Lock Record in the mark word of the object header through the CAS instruction (data is exchanged). If the object is in a lock-free state, the modification is successful, which means that the thread has obtained a lightweight lock.

image-20230504173611219

3. If the current thread already holds the lock, it means that this is a lock reentrancy. Set the first part of Lock Record to null, which acts as a reentrancy counter.

image-20230504173922343

4. If the CAS modification fails, it means that competition has occurred and needs to be expanded into a heavyweight lock.

Unlocking Process

1. Traverse the thread stack and find all Lock Records whose obj fields are equal to the current lock object.

2. If the Mark Word of the Lock Record is null, it means that this is a re-entry. Set obj to null and then continue.

image-20230504173955680

3. If the Mark Word of Lock Record is not null, use the CAS instruction to restore the mark word of the object header to a lock-free state. If it fails, it will expand to a heavyweight lock.

image-20230504174045458

5. Bias lock

When there is no competition for lightweight locks (just this thread), CAS operations still need to be performed every time reentry occurs.

Biased locking was introduced in Java 6 for further optimization: only use CAS for the first time to set the thread ID to the Mark Word header of the object, and later found that

If this thread ID is your own, it means there is no competition and there is no need to re-CAS. As long as no competition occurs in the future, this object will be owned by the thread.

static final Object obj = new Object();

public static void m1() {<!-- -->
    synchronized (obj) {<!-- -->
        // sync block A
        m2();
    }
}

public static void m2() {<!-- -->
    synchronized (obj) {<!-- -->
        // sync block B
        m3();
    }
}

public static void m3() {<!-- -->
    synchronized (obj) {<!-- -->

    }
}

Lock process

1. Create a Lock Record in the thread stack and point its obj field to the lock object.

image-20230504174525256

2. Use the CAS instruction to store the thread id of the Lock Record in the mark word of the object header, and also set the bias lock identifier to 101. If the object is in a lock-free state, the modification is successful, which represents the thread. Obtained bias lock.

image-20230504174505031

3. If the current thread already holds the lock, it means that this is a lock reentrancy. Set the first part of Lock Record to null, which acts as a reentrancy counter. Different from lightweight locks, the cas operation will not be performed again here. It only determines whether the thread id in the object header is itself. Because of the lack of cas operations, the performance is better than that of lightweight locks.

image-20230504174736226

Unlocking process refers to lightweight lock

6. Summary

Synchronized in Java has three forms: biased lock, lightweight lock, and heavyweight lock, which respectively correspond to the three situations where the lock is held by only one thread, different threads alternately hold the lock, and multi-threads compete for the lock.

Description
Heavyweight Lock The Monitor implementation used at the bottom layer involves switching between user mode and kernel mode and process context switching, which is costly and has low performance.
Lightweight lock The thread locking time is staggered (that is, there is no competition), and lightweight locks can be used to optimize. The lightweight version modifies the lock flag of the object header, and its performance is much improved compared to the heavyweight version. Each modification is a CAS operation, ensuring atomicity
Biased lock The lock is only used by one thread for a long period of time and can be used With a biased lock, there will be a CAS operation when the lock is acquired for the first time. After that, the thread only needs to determine whether the mark word is its own thread id when acquiring the lock, instead of the relatively expensive CAS command

Once lock competition occurs, it will be upgraded to a heavyweight lock