See how I explained the synchronized lock upgrade process to the interviewer

  •   Prepare for 2022 spring recruitment or summer internship. I wish you a little progress every day! Java Concurrent Programming Day8
  • This article summarizes how to avoid creating unnecessary objects in Java, which will be updated daily~
  • For knowledge points such as "let's learn Redis together" and "let's learn HarmonyOS together", you can check my previous blogs
  • Believe in yourself, the more you live, the stronger you are. You should open a way to the mountains and build a bridge in case of water! Life, you give me pressure, I return you miracle!

catalogue

1. Introduction

2. Lock upgrade

2.1 unlocked state

2.2 deflection lock

2.3 lightweight lock

2.4 heavyweight lock

1. Introduction

synchronized is a veteran in the field of Java concurrency. It can be used by many programmers. It has three forms.

  • The normal synchronization method - > synchronized locks the current object
private synchronized void demo() {
    // todo
}
  • Static synchronization - > synchronized locks the Class object of the current Class
private static synchronized void demo() {
     // todo
}
  • Synchronized code block - > synchronized locks the objects declared in code block brackets
//  Locked is the Class object of the SynchronizedDemo Class
private void demo1() {
    synchronized (SynchronizedDemo.class) {
        // todo
    }
}
//  The locked object can be the current object or any object. Every object in Java can be used as a lock
private void demo1() {
    synchronized (this) {
        // todo
    }
}

In the eyes of many people, synchronized is a concurrent implementation method with low performance. In early Java, synchronized was indeed a heavyweight lock. After JDK1.6, synchronized was comprehensively optimized. The main idea of optimization is: there is no multi process competition in most scenarios of synchronized code blocks. Generally speaking, this lock is not needed in most cases. Therefore, JDK1.6 introduces "biased lock" and "lightweight lock". Since then, there are four lock states in synchronized: no lock state, biased lock state, lightweight lock state and heavyweight lock state. The four states of a lock are a process of upgrading and playing strange. They can only be upgraded continuously rather than degraded. When a lock has become a heavyweight lock, it cannot return to a lightweight lock.

2. Lock upgrade

2.1 unlocked state

In the 32-bit virtual machine, the composition of Mark Word in the unlocked object header is as follows (if you don't know about the object header, you can see the Monitor article in this column)

In 64 bit virtual machines, the composition of Mark Word in the lock free object header is as follows (there is no difference in the implementation logic between the upgrade process of the corresponding lock of 32-bit virtual machines and 64 bit virtual machines. At present, most operating systems are 64 bit operating systems, so 64 bit virtual machines are more widely used)

The initial state of an object is unlocked. We mainly focus on the last three bits:

  • biased_lock occupies one bit, indicating whether it is a biased lock. The initial value is 0, indicating that it is not a biased lock
  • lock_state occupies two bits, indicating the lock flag bit or lock state. The initial value is 10, indicating the unlocked state

The bias lock is turned on by default, so if the bias lock is not turned off, the above biased_ The lock value should be 1.

2.2 deflection lock

Why design a lock?

This is because in most cases, there is no competition in the synchronization code at all, that is, a lock is always locked, executed and unlocked by the same thread. Can this situation be optimized? Of course! How to optimize it? This is what I do.
For example, the following code requires two lock unlocking operations when the bias lock is not used, and these operations are exempted when the bias lock is used.

final Object lock = new Object();

private void lockFirst() {
    //  Use CAS to set the thread ID to Mark in the object header   In Word
    synchronized (lock) {
        // todo
    }
    lockSecond();
}


private void lockSecond() {
    //  Compare Mark of lock object header   Is Word biased towards the current thread
    synchronized (lock) {
        // todo
    }
}

What is a bias lock?

Literally, a biased lock is a lock that biases a thread. Just mark the lock object as the current thread. How can threads be distinguished? Just use the thread ID as a marker. Just get the thread ID into the lock object!

How to achieve it?

When a thread accesses the synchronization code and needs to obtain a lock, it no longer directly associates a monitor object, but uses CAS to set the thread ID to Mark Word in the object header, and the lock biased thread ID will also be stored in the lock record in the thread stack frame. In this way, as long as the lock does not compete and the same thread attempts to obtain the same lock many times, Just compare whether the Mark Word of the lock object header is biased towards the current thread.
In the 32-bit virtual machine, the Mark Word in the object header of the bias lock is composed as follows:
The first 23 bits are set to the ID of the biased thread, biased_lock is set to 1, which indicates that the current lock object is in the biased lock state. Note that the lock flag bit of the biased lock is the same as that of the non lock flag bit, both of which are 10

In the 64 bit virtual machine, the Mark Word in the object header of the bias lock is composed as follows:
The first 54 bits are set to bias lock ID, biased_lock is set to 1

Seeing is believing, how to check?

jdk provides corresponding classes to view the memory information of print objects and introduces jol dependency

<dependency>
    <groupId>org.openjdk.jol</groupId>
    <artifactId>jol-core</artifactId>
    <version>LATEST</version>
</dependency>

Test code:

public static void main(String[] args) throws InterruptedException {

    Object lock = new Object();
    log.info(ClassLayout.parseInstance(lock).toPrintable());

}

Output results:

It can be seen that my virtual machine is a 64 bit virtual machine, because the Mark Word of the object header occupies 8 bytes, but how can the output Mark Word value be 001, not 101? Didn't you say that the bias lock is turned on by default?
This is because the virtual machine adopts delayed startup when the bias lock is turned on. However, this delayed startup bias lock can be turned off through the VM parameter - XX:BiasedLockingStartupDelay=0.

At this time, the value of MarkWord is 0x0000000000000005, which is converted to binary 101, which proves the above knowledge points. In addition, we can turn off the bias lock by - XX:-UseBiasedLocking (- XX:+UseBiasedLocking is to start the bias lock).
Turn off delay skew lock and turn off skew lock - XX:-UseBiasedLocking -XX:BiasedLockingStartupDelay=0

From the output, the bias lock is disabled

Display of bias lock

The power of biased lock is that when the lock is biased towards an object, you only need to compare the thread id. see a test code:

static final Object lock = new Object();

public static void main(String[] args) throws InterruptedException {

    log.info("Initial state...");
    log.info(ClassLayout.parseInstance(lock).toPrintable());
    Thread t1 = new Thread(() -> {
        synchronized (lock) {
            log.info("thread  t1 When holding the lock for the first time...");
            log.info(ClassLayout.parseInstance(lock).toPrintable());
        }

        synchronized (lock) {
            log.info("thread  t1 When holding the lock for the second time...");
            log.info(ClassLayout.parseInstance(lock).toPrintable());
        }

    }, "Thread-1");

    t1.start();
    t1.join();
    log.info("thread  t1 After locking...");
    log.info(ClassLayout.parseInstance(lock).toPrintable());

}

A total of 64 bits, please fill 0 in your mind
The initial Mark Word is 0x0000000000000005 - > binary 1000000000000000000000101
After thread t1, it is 0x000000002022b805 - > binary 1000000100000001011100000000101
You can see that the thread id is marked in Mark Word at this time (note that this thread id is the thread id allocated by the operating system, not the thread id given by java in the virtual machine. I don't believe you try it). To repeatedly obtain the same lock t1 thread, you only need to compare the thread id.

Relationship between hashcode() and skew lock

Let's start with a test code

@Slf4j
public class HashCodeAndBiasedLock {

    static final Object lock = new Object();

    public static void main(String[] args) {
        log.info("call HashCode before");
        log.info(ClassLayout.parseInstance(lock).toPrintable());
        lock.hashCode();
        log.info("call HashCode after");
        log.info(ClassLayout.parseInstance(lock).toPrintable());
    }

}

Output results:

You can see the change of Mark Word (64 bits in total, please fill 0 in your mind for the high bit):

Initial default opening bias lock 0x0000000000000005 - > 1000000000000000000000101

  • Call hashcode() method 0x000000063e31ee01 - > 11000111111000110001111101111000000001

  • Thread id0x063e31ee - > 11000111111000110001111101110

You can see that after calling the hashCode() method, the lowest three bits of MarkWord are converted from 101 to 001, and the bias lock is cancelled. At this time, the composition of Mark Word is shown in the following figure.


Therefore, it can be concluded that when we call the hashCode() method of a lock object, the default biased locking mechanism will be cancelled.

Upgrade bias lock to lightweight lock

When will bias locks be upgraded to lightweight locks?
You can see the following code. After t1 locks the lock object, t2 locks the lock object.

static final Object lock = new Object();
public static void main(String[] args) throws InterruptedException {

    log.info("Initial state...");
    log.info(ClassLayout.parseInstance(lock).toPrintable());
    Thread t1 = new Thread(() -> {

        log.info(String.valueOf(Thread.currentThread().getId()));
        synchronized (lock) {
            log.info("thread  t1 When holding the lock for the first time...");
            log.info(ClassLayout.parseInstance(lock).toPrintable());
        }

    }, "Thread-1");


    t1.start();
    t1.join();

    log.info("thread  t1 After locking...");
    log.info(ClassLayout.parseInstance(lock).toPrintable());

    Thread t2 = new Thread(() -> {
        synchronized (lock) {
            log.info("thread  t2 When holding the lock...");
            log.info(ClassLayout.parseInstance(lock).toPrintable());
        }
    }, "Thread-2");

    t2.start();
    t2.join();
    log.info("thread  t2 After releasing the lock...");
    log.info(ClassLayout.parseInstance(lock).toPrintable());

}

You can see that t2 thread holds the lock after t1 thread. At this time, the lock is upgraded from biased lock to lightweight lock. After t2 releases the lock, the lock does not change biased lock, but returns to lightweight lock state, which also shows that the lock upgrade process is irreversible.

2.3 lightweight lock

Introduction to lightweight lock

In my article "illustration of stack frame", there is such a diagram. When each thread runs, the virtual opportunity allocates a piece of stack memory for each thread in the virtual machine stack. Each method executed by the thread will push a stack frame into the process stack. There is a part in the stack frame composition structure of each method called lock record.

 


Were you thinking, what is this lock record for?
In fact, this lock record is used to solve lightweight locks.

When the lock is in the lightweight lock state, before the thread executes the synchronization code, the JVM will create a space for storing the lock record in the stack frame of the current thread - this is the lock record, copy the Mark Word in the lock object header into the lock record, and then try to replace the Mark Word in the object header with a pointer to the lock record through CAS, If the replacement is successful, the current thread obtains the lock. If it fails, it attempts to acquire the lock by spin. There are two cases of success and failure. If the spin acquisition lock fails, the lock expands into a heavyweight lock.

In the 32-bit virtual machine, Mark Word in the object header of the lightweight lock is composed as follows:

 


In the 64 bit virtual machine, the Mark Word of the object header in the lightweight lock is composed as follows:

See a lightweight lock code:

static final Object lock = new Object();
public static void main(String[] args) {

    log.info(ClassLayout.parseInstance(lock).toPrintable());
    synchronized (lock) {
        log.info(ClassLayout.parseInstance(lock).toPrintable());
    }
}

For the convenience of demonstration, the bias lock is turned off through the VM parameter - XX:-UseBiasedLocking, which is initially in the unlocked state. When obtaining the lock, the pointer to the lock address is recorded in Mark Word (note that the last two bits 00 represent the lightweight lock and the others represent the lock address pointer)

Specific implementation mode

As mentioned above, when a lightweight lock is used, the JVM will try to set the lock record address of the stack frame to the Mark Word of the lock object header. How to replace it?
First, let's look at the internal structure of lock records:

  • The Lock Record has a Lock Record address. The Lock Record address is used to record the memory address of the Lock Record. The current thread replaces the Mark Word of the lock object through CAS. If it succeeds, the first 62 bits of information of Mark Word will be recorded in the Lock Record address, and the Lock Record address in the Lock Record address will be written into Mark Word.
  • Object Reference is used to record the memory address of the lock object. When the lock is obtained successfully, the Object Reference is replaced by the memory address of the lock object

Let's view the locking process and unlocking process of lightweight locks through the following example code:

package com.test;

import lombok.extern.slf4j.Slf4j;
import org.openjdk.jol.info.ClassLayout;

/**
 * @Author: Liziba
 * @Date: 2021/12/4 22:50
 */
@Slf4j
public class ThinLockingDemo {

    static final Object lock = new Object();
    public static void main(String[] args) throws InterruptedException {
        log.info("Initial state...");
        log.info(ClassLayout.parseInstance(lock).toPrintable());
        Thread t1 = new Thread(() -> {
            method1();
        }, "Thread-1");

        t1.start();
        t1.join();
        log.info("thread  t1 After releasing the lock...");
        log.info(ClassLayout.parseInstance(lock).toPrintable());

    }

    private static void method2() {
        synchronized (lock) {
            log.info("thread  t1 When holding the lock for the second time...");
            log.info(ClassLayout.parseInstance(lock).toPrintable());
        }
    }

    private static void method1() {
        log.info(String.valueOf(Thread.currentThread().getId()));
        synchronized (lock) {
            log.info("thread  t1 When holding the lock for the first time...");
            log.info(ClassLayout.parseInstance(lock).toPrintable());
        }

        method2();
    }

}

First, after the t1 thread is started, when the method1() method is executed, the virtual opportunity is to push a stack frame into the virtual machine stack of the t1 thread, and there will be a Lock Record lock record in the stack frame. At this time, the relationship between the Lock Record and the lock object in the stack frame is as follows:

t1 thread attempts to replace the Mark Word of the object header of the Lock object with its own Lock Record address through CAS, and saves the value in the Mark Word into the Lock Record, and the Object Reference will record the memory address of the Lock object. At this time, the relationship between the Lock Record and the Lock object in the stack frame is as follows:

Next, method1() calls method2(), that is, lightweight lock reentry. At this point, CAS replacement is unsuccessful, because the Mark Word of the lock object has been replaced, and the locked state and the marked bit 00. However, the current thread finds that the Lock Record address is the Lock Record address in its own stack frame, and it will be in method2(). A Lock Record with null Lock Record address is added to the stack frame of. At this time, the relationship between the Lock Record and the lock object in the stack frame is shown in the figure:

Finally, after the execution of method1 method, method1 will replace the data in Mark Word of the object header of the lock object through CAS when releasing the lock. If CAS succeeds, it will return to the initial state. However, there is a case of CAS failure. When thread t1 holds the lock object, other threads fail to obtain the lock, and the lock will expand into a heavyweight lock, This process is as follows:

As shown below, t1 thread replacement succeeds and t2 thread replacement fails (t2 thread will not fail at one time, but will adapt to spin CAS for a certain number of times. If the replacement fails, the replacement fails):

At this time, Lock inflation will occur. This inflation process is completed by t2. t2 thread will associate a monitor object with the current Lock object, replace the object head of the Lock object with the memory address of the monitor, modify the Lock status bit to 10, and then enter the EntryList waiting queue of the monitor to block and wait to be awakened to compete for the Lock again. This process is as follows:

When the t1 thread ends running and attempts to replace the Mark Word of the Lock object Lock through CAS, the replacement will fail because the Mark Word of the Lock object Lock has been changed to the address of the Monitor by the t2 thread. What should I do? At this time, we have to follow the heavyweight Lock process. t1 thread will find the Monitor object according to the Monitor memory address in Mark Word, set the Owner to null, and wake up the thread blocked in EntryList. At this time, t2 can re participate in the competition for the Lock object. At this time, the Lock object has expanded into a heavyweight Lock.

Finally, let's look at the lock expansion process through the code:

package com.test;

import lombok.extern.slf4j.Slf4j;
import org.openjdk.jol.info.ClassLayout;

import java.util.concurrent.TimeUnit;

/**
 * @Author: Liziba
 * @Date: 2021/12/5 00:21
 */
@Slf4j
public class ThinLockingDemo {

    static final Object lock = new Object();
    public static void main(String[] args) throws InterruptedException {
        log.info("Initial state...");
        log.info(ClassLayout.parseInstance(lock).toPrintable());
        Thread t1 = new Thread(() -> {
            method1();
        }, "Thread-1");

        Thread t2 = new Thread(() -> {
            method2();
        }, "Thread-2");

        t1.start();
        // Get lock before running t1
        TimeUnit.SECONDS.sleep(1);
        t2.start();
        t1.join();
        t2.join();
        log.info("thread  t1 After releasing the lock...");
        log.info(ClassLayout.parseInstance(lock).toPrintable());

    }

    private static void method1() {
        synchronized (lock) {
            log.info("thread  t1 When holding the lock...");
            log.info(ClassLayout.parseInstance(lock).toPrintable());
            try {
                // t1 sleeps for 5 seconds, allowing t2 to compete for locks
                TimeUnit.SECONDS.sleep(5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    private static void method2() {
        synchronized (lock) {
            log.info("thread  t2 When holding the lock...");
            log.info(ClassLayout.parseInstance(lock).toPrintable());
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

}

2.4 heavyweight lock

Heavyweight lock, just read my Monitor article!

Keywords: Java Back-end Concurrent Programming

Added by monotoko on Wed, 08 Dec 2021 02:54:29 +0200