Synchronized locks are classified into three categories: heavyweight, lightweight, and biased. Synchronized uses different locking mechanisms in different situations.

Java object header:

The following uses a 32-bit VM as an example:

Common objects:Array objects:

Compared to a normal object, the object header of an array object has 96 bits. The extra 32 bits are used to store the length of the array.

When a lock object is acquired, the MarkWord section of the lock object header stores a pointer to the Monitor object.

As you can see, when Biased locking is turned on, the Mark Word area changes a little bit compared to the normal state. The area for storing hashCode is changed to storing Thread and epoch, and the BIASed_lock changes from 0 to 1. When upgraded to Lightweight Locked, the upper 30 bits are changed to store a lock record and the lower 2 bits are changed from 01 to 00. When expanded to weight Locked, the upper 30 bits store Pointers to associated Monitor objects and the lower 2 bits are changed from 00 to 10.

Heavyweight locks:

Monitor: Each object can be associated with a Monitor object. The structure of the Monitor object is shown in the following figure. When synchronized locks a lock object, The MarkWord area of the lock object’s object header is set as a pointer to the Monitor object.

  1. The Owner is null at first. When Thread-2 executes synchronized to acquire the lock object, the Owner property of Monitor associated with the lock object will be set to Thread-2, that is, the Thread acquiring the lock object.
  2. When Thread-2 obtains the lock object and executes the synchronized code block, if other threads execute to synchronized, the other threads will enter the EntryList queue and wait because the Thread that obtains the lock does not release the lock and the Owner is not null, that is, Thread-3, 4 and 5 as shown in the figure.
  3. When Thread-2 executes a synchronized block of code, the wait method on the lock object is called to wait in the WaitSet area, releasing the lock and waking up the waiting threads in the EntryList.
  4. Once the lock is released, the thread in the waiting queue and the new thread attempt to compete for the lock. The thread takes possession and becomes the Owner of the Monitor object, while the other threads continue to wait in the EntryList queue (Blocked).
  5. When the notify() or notifyAll method of the lock object is called when the other thread that holds the lock executes the synchronized code block, the awakened thread in the WaitSet region enters the EntryList queue to wait for the lock to be released and compete.

When Java source code is compiled into bytecode, synchronized blocks are preceded by monitorenter and Monitorexit directives. It sets the MarkWord area of the lock object header to a pointer to Monitor and resets it to wake up EntryList

Lightweight lock:

When multiple threads access a shared resource at staggered times, we can optimize for lightweight locking. The usage syntax of lightweight locks is still the synchronized keyword.

  1. First, when a thread executes a method, a frame will be opened in the virtual machine stack, and a Lock Record object will be created in the frame. The Record object can store the Mark Word of the Lock object.

2. Reference in the lock record points to the lock object. Meanwhile, cas is used to replace the Mark Word value of the lock object with the address of the lock record in the current stack frame, and the Mark Word value is stored in the lock record, which is equivalent to exchanging the address of the lock record and the Mark Word value.

3. If the CAS operation succeeds and the lock object header stores the recorded address and status 00, the lock is successfully added to the thread.

  1. If cas fails, there are two scenarios:
  • Other threads already hold the lightweight lock of the object, indicating that there is a thread contention, and the process of lock inflation enters.
  • The current thread has acquired the lightweight lock of the object, and this time it is reentrant, so it adds another lock record, which is used as the count of the lock reentrant, and the reference of the lock record also points to the lock object.

  1. When synchronized locks a lock record with a value of NULL, it is known that this is a lock reentrant and the count is reduced by one.
  2. If you find a lock record whose value is not null, try using CAS to restore the Mark Word value to the object header.
  • The lock is successfully released. Procedure
  • If the recovery fails, lock inflation occurs, and the heavyweight lock is unlocked.

Lock expansion:

When a thread fails to add a lightweight lock, another thread may have added a lightweight lock to the object, indicating that a race occurs and the lock expansion process enters.

  1. If Thread-1 attempts to apply a light lock to an object when Thread0 has already applied a light lock to the object

  1. Thread-1 failed to add a light lock and entered the lock expansion process:
  • The lock object is associated with a Monitor object, and the Mark Word area of the object header is set to the address of the Monitor object.
  • If thread-1 fails to lock, it enters the EntryList of the Monitor and waits. The Owner of the Monitor is set to Thread-0.

  1. When thread-0 completes the lock release, cas is used to restore the Mark Word value to the lock object header, which fails. Enter the unlock process of heavyweight locks, set the Owner of Monitor to NULL, and wake up the waiting threads in the EntryList.

Biased locking:

When there is no competition from other threads, the cas operation is required every time the lock is added and unchained, which will also consume certain resources. Therefore, biased lock is introduced in Java6 to optimize. When the lock is added for the first time, CAS will put the current thread ID in the MarkWord area of the lock object header, and will not restore the MarkWord area in the subsequent unlocking. As long as the thread ID is found to be its own, cas operation is not required. As long as there is no competition from other threads, cas will keep the lock change.

Looking back at our object header above, when the object is created:

  • If biased locking is enabled (which is enabled by default), then the last three digits of the MarkWord are 101, and its Thread, epoch, and age are 0
  • Biased locking is delayed by default, when won’t start immediately open, if want to avoid delay, you can use a virtual machine parameters – XX: BiasedLockingStartupDelay = 0 to disable delay
  • If the object is created without bias locking enabled, then the last three digits of MarkWord are 001, its hashcode and age are both zero, and hashcode is assigned only the first time it is called.

Partial lock undo:

  1. The bias lock is revoked when an object’s Hashcode is called, because there is no hashCode area in the object header of the bias lock. Lightweight locks record Hashcode in the lock record, and heavyweight locks record Hashcode in Monitor.
  2. Bias locks are upgraded to lightweight locks when other threads are competing
  3. When a lock object calls wait/notify/notifyAll, biased locks inflate to heavyweight locks because they are associated with areas in Monitor.

Batch heavy bias:

After a lock has been revoked more than 20 times in favor of a particular thread, the JVM feels that the lock has been revoked in favor of the wrong thread, and the next time the lock is revoked, the JVM will favor the new thread that acquired the lock. (Undo 20 times doesn’t have to be the same lock, it can be multiple locks or even 20 locks.)

Batch undo:

When a biased lock has been revoked more than 40 times, the JVM decides that biased locking should not be used at all, and biased locking is turned off for all objects and is not turned on by default for newly created objects.