Advanced Synchronized principle (lightweight lock, lock expansion, spin lock, bias lock)

Advanced Synchronized principle (lightweight lock, lock expansion, spin lock, bias lock)

1. Lightweight lock

Usage scenarios of lightweight locks:
If an object is accessed by multiple threads, but the access time of multiple threads is staggered (that is, there is no competition), lightweight lock optimization can be used.
Lightweight locks are transparent to users, and the syntax is still synchronized.

Suppose there are two methods to synchronize blocks and lock with the same object

public class TestLightWeightLock {
    static final Object obj = new Object();

    public static void f1() {

        synchronized (obj) {
            //Synchronization block A
            f2();
        }
    }

    //Lock reentry
    public static void f2() {
        synchronized(obj){
            //Synchronization block B
         }
    }
}

[1] When creating a Lock Record object, the stack frame of each thread will contain a Lock Record structure, which can store the MarkWord of the lock object.


[2] Let the Object reference of the lock record point to the lock Object, try to replace the Object's MarkWord with CAS (Compare And Swap), and store the MarkWord value in the lock record.



[3.1] if CAS exchange is successful (the object header is 01), the lock record address and status 00 are stored in the object header, indicating that the thread locks the object. At this time, the figure is as follows:


[3.2] if CAS fails, there are two situations:
If another thread already holds the lightweight lock of the Object, it indicates that there is competition and enters the lock expansion process.
If the synchronized lock reentry is performed by yourself, add another Lock Record as the reentry count.


[4.1] when exiting the synchronized code block (when unlocking), if there is a lock record with a value of null, it indicates that there is reentry. At this time, reset the lock record, indicating that the reentry count is - 1.

[4.2] when exiting the synchronized code block (when unlocking), the lock record is not null. In this case, CAS is used to restore the value of MarkWord to the lock object.

  • If successful, the unlocking is successful.
  • Failure indicates that the lightweight lock has been expanded or upgraded to a heavyweight lock, and enters the unlocking process of the heavyweight lock.

2. Lock expansion

If CAS operation fails when trying to add a lightweight lock, then another thread adds a lightweight (competing) to the secondary object. At this time, lock expansion is required to change the lightweight lock into a heavyweight lock.

[1] When Thread-1 performs lightweight locking, Thread-0 has already applied lightweight locking to the object.


[2] At this time, Thread-1 fails to add a lightweight lock and enters the lock expansion process.
That is, apply for the Monitor lock (heavyweight lock) for the Object object, and let the Object point to the heavyweight lock address.
The Owner of the Monitor object is set to Thread-0, the thread that owns the lock object at this time.
Then enter the EntryList BLOCKED (blocking queue) of the Monitor object.

[3] When Thread-0 exits the synchronization block unlocking, CAS is used to restore the value of Mark Word to the object header. At this time, it will fail (because the MarkWord of the lock object stores the address of Monitor). At this time, it will enter the heavyweight unlocking process, that is, find the Monitor object according to the Monitor address, set the Owner to null, and wake up the BLOCKED thread in the EntyList.

3. Spin optimization

When competing for heavyweight locks, you can also use spin to optimize. If the current thread spins successfully (that is, the lock holding thread has exited the same block and released the lock), the current thread can avoid blocking.
Blocking requires context switching and consumes resources.
Suitable for multi-core CPU, single core CPU is meaningless.


Successful spin retry:


Spin failure:


After Java 6, the spin lock is adaptive. For example, if the object has just succeeded in a spin operation, it is considered that the spin is very likely to succeed this time, and it will spin several more times; On the contrary, less spin or even no spin.
Spin will occupy CPU time. Single core CPU spin is a waste, and multi-core CPU spin can give play to its advantages.
After Java 7, you can't control whether to turn on the spin function.

4. Deflection lock

When the lightweight lock has no competition (just its own thread), CAS operation still needs to be performed each time it re enters.
The bias lock introduced in Java 6 is used for further optimization: only the first time CAS is used to set the thread ID to the Mark Word header of the object, and then it is found that the thread ID is its own, indicating that there is no competition and there is no need to re CAS. As long as there is no competition, the object belongs to the thread.

package com.concurrent.p2;

/**
 * Bias lock
 */
public class TestBiasedLock {
    static final Object obj = new Object();

    public static void f1() {

        synchronized (obj) {
            //Synchronization block A
            f2();
        }
    }

    //Lock reentry
    public static void f2() {
        synchronized(obj){
            //Synchronization block B
            f3();
        }
    }

    //Lock reentry
    public static void f3() {
        synchronized (obj){
            //Synchronization block C
        }
    }

}




Lightweight lock: when synchronized is called for the first time, a lock record RecordLock will be generated. CAS operation will be used to replace the lock record with the object's markword (the object is from 01 to > 00); After that, call synchronized again and try to replace the lock record and markword with CAS operation again. Since the replacement has been carried out for the first time, the subsequent replacement will not succeed, but a lock record will still be generated for counting.


The bias lock is used to optimize the lightweight lock. Only the first time CAS is used to set the thread ID to the object markword header. After that, if the thread ID is the object's own, there is no competition and CAS is not required. As long as there is no competition, the thread is owned by the object. (thread biased object)

4.1 object header format MarkWord


When an object is created:
1) If the bias lock is enabled (it is enabled by default), after the object is created, the markword value is 0x05, that is, the last three bits are 101. At this time, its thread, epoch and age are all 0.
2) Bias lock is default and delayed and will not take effect immediately when the program is started. If you want to avoid delay, you can add JVM parameters:
-XX:BiasedLockingStartupDelay=0
To disable the delay.
3) If the bias lock is not turned on, after the object is created, the value of markword is 0x01 (the last three bits are 001). At this time, its hashcode and age are all 0. It will be assigned when hashcode is used for the first time.

4.2 bias lock status

Create a Dog object and view the bias lock in markword

/**
 * To create an object, 101 indicates that a bias lock can be used
 * <p>
 * If you want to avoid delayed loading, add the JVM command
 * -XX:BiasedLockingStartupDelay=0
 */
@Test
public void t1() throws InterruptedException {
    //markword
    log.debug(ClassLayout.parseInstance(new Dog()).toPrintable());
    //Delay 4 seconds
    Thread.sleep(4000);
    log.debug(ClassLayout.parseInstance(new Dog()).toPrintable());
}

After adding JVM parameters:

Put bias lock on object

/**
 * Plus bias lock
 */
@Test
public void t2() {
    Dog d = new Dog();
    //Before locking
    log.debug(ClassLayout.parseInstance(d).toPrintable());
    //Locking
    synchronized (d) {
        log.debug(ClassLayout.parseInstance(d).toPrintable());
    }
    //After locking
    log.debug(ClassLayout.parseInstance(d).toPrintable());
}


After the bias lock is added, the ID of the main thread is always stored in the markword of the object.

When bias lock is disabled, lightweight lock is used
JVM parameters:
-XX:-UseBiasedLocking

/**
 * Disable skew lock JVM parameters
 * -XX:-UseBiasedLocking
 */
@Test
public void t3() {
    Dog d = new Dog();
    //Before locking
    log.debug(ClassLayout.parseInstance(d).toPrintable());
    //Locking
    synchronized (d) {
        log.debug(ClassLayout.parseInstance(d).toPrintable());
    }
    //After locking
    log.debug(ClassLayout.parseInstance(d).toPrintable());
}

4.3 undo - call the hashCode method of the object

The hashCode of the object is called, but the thread id is stored in MarkWord for the lock biased object. If the hashCode is called, the lock biased will be revoked.
The lightweight lock will record the hashCode in the lock record;
The heavyweight lock will record the hashCode in the Monitor;
Use bias lock after calling hashCode. Remember to remove - XX:-UseBiasedLocking (JVM command to disable bias lock)

Calling the hashCode() method of the object disables the bias lock

/**
 * hashCode() method for calling objects before synchronization.
 * <p>
 * Turn off delayed loading - XX:BiasedLockingStartupDelay=0
 */
@Test
public void t4() {
    Dog d = new Dog();
    d.hashCode();   //Disables the object's bias lock
    //Before locking
    log.debug(ClassLayout.parseInstance(d).toPrintable());
    //Locking
    synchronized (d) {
        log.debug(ClassLayout.parseInstance(d).toPrintable());
    }
    //After locking
    log.debug(ClassLayout.parseInstance(d).toPrintable());
}

Why does calling the hashCode() method disable biased locking?

Because there is no place to store 31 bit hashCode value in MarkWord with bias lock. Most of the space in the partial lock is used to store the thread ID.
In the lightweight lock, the hashCode of the object will be stored in the lock record of the thread stack frame.
In the heavyweight lock, the hashCode of the object will be stored in the Monitor object and can be restored after unlocking.

4.4 undo - objects used by other threads

When another thread uses a biased lock object, the biased lock is demoted to a lightweight lock.

 /**
     * Undo - other threads use the lock object
     * <p>
     * Turn off delayed loading - XX:BiasedLockingStartupDelay=0
     */
    static final Object lock = new Object();

    @Test
    public void t5() throws InterruptedException {
        Dog d = new Dog();
        new Thread(() -> {
            log.debug(ClassLayout.parseInstance(d).toPrintable());
            synchronized (d) {
                log.debug(ClassLayout.parseInstance(d).toPrintable());
            }
            log.debug(ClassLayout.parseInstance(d).toPrintable());
            synchronized (lock) {
                lock.notify();
            }
        }, "t1").start();
        new Thread(() -> {
            synchronized (lock) { //Separate the two threads
                try {
                    lock.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            log.debug(ClassLayout.parseInstance(d).toPrintable());
            synchronized (d) {
                log.debug(ClassLayout.parseInstance(d).toPrintable());
            }
            log.debug(ClassLayout.parseInstance(d).toPrintable());
        }, "t2").start();

        Thread.sleep(10000);
    }
}

Thread 1: synchronized(b),


Thread 2: since the lock object has recorded the ID of thread 1, when synchronized(b) is called again, thread 2 and thread 1 have a competitive relationship, and the biased lock is degraded to a lightweight lock.

4.5 undo - call wait/notify

Only heavyweight locks have wai/notify. When these methods are called, they change from biased locks to heavyweight locks.

/**
 * Undo - wait/notify
 * Shift lock to heavyweight lock
 * Turn off delayed loading - XX:BiasedLockingStartupDelay=0
 */
@Test
public void t6() throws InterruptedException {
    Dog d = new Dog();
    //Thread 1
    new Thread(() -> {
        log.debug(ClassLayout.parseInstance(d).toPrintable());
        synchronized (d) {
            try {
                d.wait();   //Thread t1 waiting
                log.debug(ClassLayout.parseInstance(d).toPrintable());
                log.debug("The thread is awakened....");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        log.debug(ClassLayout.parseInstance(d).toPrintable());
    }, "t1").start();
    //Thread 2
    new Thread(() -> {
        log.debug(ClassLayout.parseInstance(d).toPrintable());
        synchronized (d) {
            try {
                Thread.sleep(1000); //Wake up thread t1 after 1000 seconds
                log.debug(ClassLayout.parseInstance(d).toPrintable());
                d.notify();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        }
        log.debug(ClassLayout.parseInstance(d).toPrintable());
    }, "t2").start();

    Thread.sleep(10000);
}

Thread t1, from biased lock to heavyweight lock:


Thread t2, heavyweight lock:

4.6 batch reorientation

If the object is accessed by multiple threads, but there is no competition, the object biased to thread T1 may still be biased to thread T2, and the re bias will reset the ThreadID of the object.
When the unbiased lock threshold is revoked more than 20 times, the jvm will think whether the bias is wrong, so it will re bias to the locking thread when locking these objects.

/**
 * Batch re bias
 * Turn off delayed loading - XX:BiasedLockingStartupDelay=0
 */
@Test
public void t1() throws InterruptedException {
    Vector<Dog> list = new Vector<>();
    Thread t1 = new Thread(() -> {
        for (int i = 0; i < 30; i++) {
            Dog d = new Dog();
            list.add(d);
            synchronized (d) {
                log.debug(i + ":\t" + ClassLayout.parseInstance(d).toPrintable());
            }
        }
        synchronized (list) {
            list.notifyAll();
        }
    }, "t1");
    t1.start();

    Thread t2 = new Thread(() -> {
        synchronized (list) {
            try {
                list.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        log.debug("=========");
        for (int i = 0; i < 30; i++) {
            Dog d = list.get(i);
            log.debug(i + ":\t" + ClassLayout.parseInstance(d).toPrintable());
            synchronized (d) {  //Batch re bias
                log.debug(i + ":\t" + ClassLayout.parseInstance(d).toPrintable());
            }
        }
    }, "t2");
    t2.start();

    Thread.sleep(10000);
}

i = 0, thread ID biased to lock is t1:


When i = 19 is reached, and the thread ID biased to lock becomes t2:

4.7 batch cancellation

When the unbiased lock threshold is revoked more than 40 times, the jvm will think that it is indeed biased wrong and should not be biased at all, so all objects of the whole class will become unbiased, and the newly created objects will also be unbiased.

/**
 * Batch undo
 * Turn off delayed loading - XX:BiasedLockingStartupDelay=0
 */
static Thread t1, t2, t3;

@Test
public void t2() {
    Vector<Dog> list = new Vector<>();
    long loopNumber = 39;
    //Thread 1
    t1 = new Thread(() -> {
        for (int i = 0; i < loopNumber; i++) {
            Dog d = new Dog();
            synchronized (d) {
                log.debug(i + ":\t" + ClassLayout.parseInstance(d).toPrintable());
            }
            list.add(d);
        }
        LockSupport.unpark(t2);
    }, "t1");
    t1.start();
    //Thread 2
    t2 = new Thread(() -> {
        LockSupport.park();
        log.debug("========================");
        for (int i = 0; i < loopNumber; i++) {
            Dog d = list.get(i);
            synchronized (d) {
                log.debug(i + ":\t" + ClassLayout.parseInstance(d).toPrintable());
            }
        }
        LockSupport.unpark(t3);
    }, "t2");
    t2.start();
    //Thread 3
    t3 = new Thread(() -> {
        LockSupport.park();
        log.debug("========================");
        for (int i = 0; i < loopNumber; i++) {
            Dog d = list.get(i);
            synchronized (d) {
                log.debug(i + ":\t" + ClassLayout.parseInstance(d).toPrintable());
            }
        }
    }, "t3");
    t3.start();
    try {
        t3.join();
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    //When creating new objects, the jvm thinks that the competition is fierce and sets the objects of the whole class to be unbiased
    log.debug(ClassLayout.parseInstance(new Dog()).toPrintable());
}

t1 thread: the lock object is biased to t1 thread.


t2 thread: undo one by one at the beginning, turning the lock object into a lightweight lock.

t2 thread: after reaching the threshold of 20, the lock object starts to re bias to t2 thread (the thread ID changes).

t3 thread: the first 20 are lightweight locks that cannot be biased.


From the 19th to the 38th, it was originally biased towards t2 thread. It switched from t2 thread to t3 thread and became a lightweight lock. Programming 01 cannot be biased after unlocking.

From the 40th object, it becomes an unbiased state.

5. Lock elimination

Without lock elimination optimization, it will lead to low operation efficiency.


Keywords: Java

Added by jOE :D on Fri, 15 Oct 2021 03:08:49 +0300