Location>code7788 >text

Deeper understanding of biased locks, lightweight locks, heavyweight locks

Popularity:108 ℃/2024-10-25 17:38:28

I. Object structure and lock state

The synchronized keyword is the built-in lock implementation in java. The built-in lock is actually an arbitrary object whose memory structure is shown below

Java对象结构

Among them, the Mark Word field occupies 64bit length under 64-bit VM and its structure is shown below

64位Mark Word的结构信息

You can see that the Mark Word field has a very important role is to record the current object lock state, the last 3bit field is used to mark the current lock state is no lock, bias lock, lightweight lock or heavyweight lock.

(1) lock: lock status mark bit, accounting for two binary bits, because of the desire to use as few binary bits as possible to express as much information, so set the lock mark. The value of the mark is different, the meaning of the whole Mark Word is different.

(2) biased_lock: whether the object is biased lock enabled or not, only occupies 1 binary bit. A value of 1 indicates that the object has biased lock enabled, while a value of 0 indicates that the object does not have biased lock.

The combination of the lock and biased_lock flag bits together indicate what kind of lock state the Object instance is in. The meaning of the combination of the two is shown in the following table

image-20240919171225689

Prior to JDK version 1.6, all Java built-in locks were heavyweight locks. Heavyweight locks cause the CPU to switch frequently between the user state and the core state, so they are costly and inefficient.JDK version 1.6 introduced the implementation of biased locks and lightweight locks in order to reduce the performance consumption caused by acquiring and releasing locks. Therefore, in JDK 1.6 version of the built-in lock has a total of four states: no lock state, biased lock state, lightweight lock state and heavyweight lock state, these states with the competition gradually upgrade. Built-in locks can be upgraded but not downgraded, meaning that a biased lock can't be downgraded to a biased lock after it has been upgraded to a lightweight lock. The purpose of this strategy of being able to upgrade but not downgrade is to improve the efficiency of acquiring and releasing locks.

With the emergence of competition and the escalation of competition, the lock state will be changed in turn from a lock-free state to a biased lock, a lightweight lock, a heavyweight lock, the following case study explains the process of this lock expansion (based on Java8).

II. No locks

The memory structure of an object in a lock-free state is shown below

image-20241018160620782

The state of an object that is not modified by synchronized is the lock-free state, see the following code

import ;
import .slf4j.Slf4j;
import ;

/**
 * @author kdyzm
 * @date 2024/10/18
 */
@Slf4j
public class NoLockTest {

    public static void main(String[] args) {
        User user = new User();
        ((user).toPrintable());
    }

    @Data
    public static class User {
        private String userName;
    }
}

Here the memory structure of the common object user is output using the JOL tool (for more details on java object memory structure, check out theDeeper Understanding of Java Object Structure》)

image-20241018155332377

As you can see, the output 64-bit MarkWord values are hexadecimal numbers0x0000000000000001 (non-biasable; age: 0) It even thoughtfully gives the current non-bias lock state and the current age of the object. Since a high version of JOL, 0.17, is used here, the output is in big-endian order, so based on the last byte, 01, you can tell that its binary number is0000 0001, that is, the values of biased_lock and lock 3 bit are001

image-20240919171225689

According to the lock status flag, you can know that the current object is lock-free. It should be noted that the output of the no-lock state is because the bias lock mechanism is not turned on, if the bias lock flag is turned on, the created object comes with the bias lock flag by default.

III. Deflection locks

A biased lock is a piece of synchronized code that is accessed by the same thread all the time, then that thread automatically acquires the lock, reducing the cost of acquiring the lock.If the built-in lock is in the biased state, when a thread comes to compete for the lock, the biased lock is used first, indicating that the built-in lock favors this thread, and this thread doesn't need to do any more checking or switching when it wants to execute the synchronization code associated with the lock. Biased locks are very efficient when there is not much competition.

Note that the biased locking state is not set the first time a lock occupancy occurs. The JVM defaults to a delayed start of the biased locking mechanism after 4 seconds of startup, at which point the biased locking flag will be added to the created object by default. This can be done by adding the JVM startup parameter:-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0, allowing the system to turn on the bias locking mechanism by default.

The Mark Word for the biased lock state records the ID of the thread that the built-in lock favors itself, and the built-in lock treats that thread as its acquaintance. The Mark Word for an object in the biased lock state is shown below

image-20241018161149485

1、Biased lock core principle

The core principle of biased locking is that if a thread for which there is no thread competition acquires the lock, then the lock enters the biased state, at which point the structure of the Mark Word changes to a biased lock structure, the lock flag bit (lock) of the lock object is changed to 01, the biased flag bit (biased_lock) is changed to 1, and then the thread's ID is recorded in the Mark Word of the lock object (using the CAS operation). In the future, when the thread acquires the lock, it will judge the thread ID and the flag bit, and then it can directly enter the synchronization block without even CAS operation, which saves a lot of operations related to the lock application, and thus also improves the performance of the program.

The main function of biased locking is to eliminate the synchronization primitive in the case of no contention and further improve program performance, so biased locking has a good optimization effect in occasions where there is no lock contention. However, once there is a second thread that needs to contend for the lock, then the bias mode immediately ends and enters the lightweight locking state.

If the synchronization block is uncontested in most cases, then performance can be improved by biasing it. That is, when there is no competition, the thread that has previously acquired the lock will judge whether the thread ID of the biased lock points to itself when it acquires the lock again, if yes, then the thread will not need to acquire the lock again, and can directly enter the synchronization block; if it does not point to the current thread, then the current thread will use the CAS operation to set the thread ID in the Mark Word to the current thread ID, if the CAS operation succeeds, then the biased lock is acquired successfully, and the synchronization code block is executed. If the CAS operation succeeds, then the bias lock is successfully acquired and the synchronization block is executed. If the CAS operation fails, then it means that there is a contention, and the lock-grabbing thread is hung up, revoking the bias lock of the lock-possessing thread, and then inflating the bias lock to a lightweight lock.

Disadvantages of biased locks: biased locks are redundant if the lock object is contested by multiple threads from time to time, and their revocation incurs some performance overhead.

2、Biased lock case

The following code prints the memory structure of a single thread before it occupies a lock, after it has occupied the lock, and after it has released the lock (JOL tool output), so that the biased lock state can be observed (note that the biased lock feature is turned on.)-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0

import ;
import ;
import ;
import .slf4j.Slf4j;
import ;


/**
 * @author kdyzm
 * @date 2024/10/18
 * At startup, be sure to add theJVMpriming parameter:-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0
 */
@Slf4j
public class BiasedLockTest {

    public static void main(String[] args) throws InterruptedException {
        User lock = new User();
        ("Grab the front of the lock.lockpresent state:\n{}", ());
        Thread thread = new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                synchronized (lock) {
                    if (i == 99) {
                        ("possessor's locklockpresent state:\n{}", ());
                    }
                }
            }
        }, "biased-lock-thread");
        ();
        ();
        ("After releasing the locklockpresent state:\n{}", ());
    }

    @ToString
    @Setter
    @Getter
    public static class User {
        private String userName;

        //Object Structure String
        public String getObjectStruct() {
            return (this).toPrintable();
        }
    }
}

output result

image-20241019215404495

Analyze the output:

Grab the front of the lock.: Printing the lock memory structure, you can see that the last byte of the05, which corresponds to the biased+lock state combination, the penultimate three bits are101This is the "biasable" state, when the lock object is not locked and is not biased, so it is "biasable". But the printout is a bit different, it's in "biasable" state, when the lock object is not yet locked, not biased, so it's in "biasable" state.

possessor's lock: After taking possession of the lock, you can see that the last byte is still05However, the output already indicates that it is "biased", i.e., in favor of the lock state, and the MarkWord of the lock already records the id of the thread that occupies the lock, but since this thread ID is not the ID of the Thread instance in Java, there is no way to compare it directly in the Java program.

After releasing the lock: You can see that the last byte is still05This is because the lock release requires a certain amount of overhead, and the biased lock is an optimistic lock, which believes that there is still a high probability that the thread holding the biased lock will continue to acquire the lock, so it will not actively revoke the biased lock state.

Think about a question: the same thread acquires the same lock repeatedly, and the lock object lock becomes a biased lock, so if the current thread ends, a new thread is created and the lock lock is reacquired, is the thread id recorded in the lock lock updated to the thread id of the new thread to achieve rebiasing?

import ;
import ;
import ;
import .slf4j.Slf4j;
import ;


/**
 * @author kdyzm
 * @date 2024/10/18
 */
@Slf4j
public class BiasedLockTest {

    public static void main(String[] args) throws InterruptedException {
        User lock = new User();
        Thread threadA = runThread(lock, "A");
        Thread threadB = runThread(lock, "B");
        ();
        ();
        ();
        ();
    }

    private static Thread runThread(User lock, String name) throws InterruptedException {
        Thread thread = new Thread(() -> {
            ("Grab the front of the lock.lockpresent state:\n{}", ());
            for (int i = 0; i < 100; i++) {
                synchronized (lock) {
                    if (i == 99) {
                        ("possessor's locklockpresent state:\n{}", ());
                    }
                }
            }
            ("After releasing the locklockpresent state:\n{}", ());
        }, name);
        return thread;
    }

    @ToString
    @Setter
    @Getter
    public static class User {
        private String userName;

        //JOLObject Structure String
        public String getObjectStruct() {
            return (this).toPrintable();
        }
    }
}

Thread A execution result:

image-20241021221902252

Thread B execution result:

image-20241021224745019

You can see that thread B waits until thread A has finished to acquire the lock lock, but the lock state does not remain in the expected biased lock state with the thread id pointing to thread B. Instead, the lock is directly upgraded to a lightweight lock.

In terms of operational results, the bias lock is more like "lit. buy and sell with a single hammer", as long as the bias is in favor of one thread, any subsequent attempts to acquire the lock by other threads will become lightweight locks, which is a very limiting bias.Actually, that's not true., to explain this, one needs to first understand the "batch rebalance"Knowledge.

3. Batch weight bias

Batch rebiasing is an optimization technique of JVM for locking objects of Class, if a Class has many objects, each object is used as an object lock, when it is first acquired by a thread, they all become biased locks; when they are all released, they will remain biased locks by default: on the one hand, to prevent the corresponding thread from acquiring the locks again for a short period of time, and on the other hand, revoke the bias. On the other hand, revoking the bias is not a big deal if there are too many of them; if another thread accesses these locks, the JVM encounters a problem: should I immediately change the thread id in the MarkWord of the lock to the thread id of the new thread?

You a new thread to get a lock, up on the heavy favor you, is not very reasonable? Anyhow, you also have to examine the examination of whether you are qualified by the JVM "favor" it, the way to examine the new thread to try to obtain the object lock how many times, assuming that the threshold is set to 20, that is, I have 20 object locks are already biased lock state, biased thread A, if this time the new thread B and to obtain these object locks, then the first 19 times will not be re-biased to thread B, directly upgrade the lightweight lock, to the 20th time when the lightweight lock, and then the thread B to obtain these object locks. lock, then the first 19 times will not be re-biased to thread B, directly upgrade the lightweight lock, to the 20th time when the thread B again, that thread B later probability will still try to obtain the lock, then the bias locks re-biased to thread B.

Run the command on the linux command line or in git bash in Windows:java -XX:+PrintFlagsFinal | grep BiasedLock View bias lock related configuration parameters

[root@lenovo ~]# java -XX:+PrintFlagsFinal | grep BiasedLock
     intx BiasedLockingBulkRebiasThreshold          = 20                                  {product}
     intx BiasedLockingBulkRevokeThreshold          = 40                                  {product}
     intx BiasedLockingDecayTime                    = 25000                               {product}
     intx BiasedLockingStartupDelay                 = 4000                                {product}
     bool TraceBiasedLocking                        = false                               {product}
     bool UseBiasedLocking                          = true                                {product}

Among other things, we have already used the UseBiasedLocking and BiasedLockingStartupDelay parameters, which were previously used to enable biased locking at the startup of the control system and to set the biased locking startup delay to zero:-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0Now, let's get down to business.BiasedLockingBulkRebiasThreshold parameters

parameters an explanation of the meaning of words or phrases
BiasedLockingBulkRebiasThreshold=20 Batch bias threshold, default value 20

This bulk bias threshold of 20 means: a class has 20 objects, thread A synchronized each object to acquire the lock and modified each object into a bias lock, thread B re-acquires these 20 object locks and tries to re-bias them, the first 19 times failed, these 19 object locks were upgraded from bias locks to lightweight locks, and on the 20th one, it reaches the bulk bias threshold, a re-bias will occur and the bias locks are upgraded from bias locks to bias locks. Re-biasing occurs, and the biased locks change from biased A thread to biased B thread.

The following is verified by code

import .slf4j.
import ;

import ;
import ;

/**
 * Biased locking - batch re-biased test
 * Be careful to add -XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0 JVM startup parameter before running.
 **/
@Slf4j
public class BulkRebiasTest {

    static class User {

    }

    public static void main(String[] args) throws InterruptedException {
        final List<User> list = new ArrayList<>();
        Thread A = new Thread() {
            Thread A = new Thread() { @Override
            public void run() {
                for (int i = 0; i < 20; i++) {
                    User a = new User();
                    (a);
                    //After acquiring the locks, they all become biased locks, favoring thread A
                    synchronized (a) {

                    }
                }
            }
        }

        Thread B = new Thread() {
            Thread B = new Thread() { @Override
            public void run() {
                for (int i = 0; i < 20; i++) {
                    User a = (i);
                    //Threads from the list are biased in favor of thread A.
                    ("B {}th time before lock" + (a).toPrintable(), i + 1); synchronized (a) {
                    synchronized (a) {
                        // the first 19 times the bias lock is revoked to favor thread A, and then the lightweight lock is upgraded to point to the lock record on thread B's thread stack
                        // the 20th time the bias lock is rebiased in favor of thread B
                        ("B locks in {}th time" + (a).toPrintable(), i + 1);
                    }
                    //because the first 19 times are lightweight locks, and are lockless unbiased after release
                    // but the 20th time is a biased lock biased towards thread B still biased towards thread B after release
                    ("B locking ends the {}th time" + (a).toPrintable(), i + 1);
                }
                // After the 20th rebiasing, the epoch field of the User class becomes 1, and the epoch field in the newly spawned object is also 1
                ("Newly generated object:" + (new User()).toPrintable());
            }

        }
        ();
        (1000);
        ().
        (1000);
    }
}

The results of the run are as follows

Results of the first 19 cycle runs of thread B:

image-20241023104105626

Results of the 20th run of thread B:

image-20241023104355105

Finally, regenerate an instance of the User class and print directly to see the result

image-20241023110816192

An important field appears here: epoch

4、epoch

The epoch field is a 2-bit field in Mark Word:

image-20241018161149485

The epoch plays an important role in bias locking by determining whether the current bias lock goes heavy bias or lock escalation logic.

everyoneclass class (computing)Maintained aBias Lock Undo CounterAs long asObjects of classoccurrenceBias reversalThe counter+1, when this value reaches the rebias threshold (default 20):BiasedLockingBulkRebiasThreshold=20The JVM considers the class to have a bias lock problem, and therefore does a bulk rebias, which is implemented using theepochFields.

Each class object will have a correspondingepochfields, eachobject in biased lock state (computing)(used form a nominal expression)mark word This field is also present in the object's initial value, which is the value of theepochvalues (the two are equal at this point).

Each time a batch rebias occurs, add 1 to this value and traverse the stacks of all threads in the JVM:

  1. Find all of the class'sIt's locked.The bias lock object of theepochField changed to new value
  2. classNot in locked statebiased lock object (not held by any thread, but previously held by a thread; the markword of such a lock object must be biased as well), keep theepoch Field values remain unchanged

This way, when a biased lock is attempted to acquire the lock by another thread, it will check theepoch in the bias lock object classwhether and howBias Lock Mark Word in epochSame:

  1. If it is not the same, it means that when the epoch in the bias lock object class was increased, the thread holding the bias lock had already ended, so the epoch in the bias lock Mark Word was not increased, i.e., theThe bias lock on this object is no longer validAt this point, you can go heavy bias logic (the name epoch "epoch" also means this, come to the new epoch, the old epoch of things do not care).
  2. If it is the same, it means that it is still the current epoch, no rebiasing has occurred in the current biased lock, and there is a new thread competing for the lock, then the lock has to be upgraded.

Going back to the bulk rebiased case above, thread B looped 20 times on the bias lock instance of the User class acquiring the lock each time. In the first 19 cycles, because the epoch of the User class is still 0, and the epoch in the Mark Word of each bias lock object is also 0, the lock upgrade process is carried out, and the bias lock is upgraded to a lightweight lock, and because the lock revocation occurs in the process of upgrading the lightweight lock, the bias lock revocation counter of the User class is +1 at the same time; in the 20th cycle, the epoch of the User class increases by 1 and becomes 20, triggering the batch rebiasing. At the 20th time, the epoch of the User class is increased by 1 and becomes 20, which triggers the batch rebiasing, and the upgrade of the lightweight lock should have gone to the batch rebiasing. After the batch rebiasing, the epoch field of the User class is increased by 1 and becomes 1, so the default epoch of the new instance of the User class is consistent with that of the User class, which is 1.

5. Batch revocation

Here is a discussion about another parameter of biased locking: BiasedLockingBulkRevokeThreshold, the

parameters an explanation of the meaning of words or phrases
BiasedLockingBulkRevokeThreshold=40 Batch Undo Threshold, default value 40

From the output, it defaults to 40

[root@lenovo ~]# java -XX:+PrintFlagsFinal | grep BiasedLock
     intx BiasedLockingBulkRebiasThreshold          = 20                                  {product}
     intx BiasedLockingBulkRevokeThreshold          = 40                                  {product}
     intx BiasedLockingDecayTime                    = 25000                               {product}
     intx BiasedLockingStartupDelay                 = 4000                                {product}
     bool TraceBiasedLocking                        = false                               {product}
     bool UseBiasedLocking                          = true                                {product}

BiasedLockingBulkRevokeThreshold is used to set the threshold for biased lock bulk revocation, when the number of biased lock revocations reaches this value two things happen

  1. The markword of the class will be modified to be unbiased and lock-free, so that objects generated by the class will become lock-free, and when threads contend for locks, they will no longer go to biased locks, and will go directly to lightweight locks.
  2. Iterates through the stacks of all currently alive threads, finds all lock instance objects of that class that are in the biased lock state, and performs a biased lock revocation.

See below for an example of validation

import .slf4j.
import ;

import ;
import ;
import ;

/**
 * @author kdyzm
 * @date 2024/10/23
 */
@Slf4j
public class BulkRevoteTest {

    static class User {

    }

    private static Thread A; private static Thread B; private static User { }
    private static Thread A; }
    private static Thread C; }

    public static void main(String[] args) throws InterruptedException {
        final List<User> list = new ArrayList<>();
        // Thread A creates 40 biased locks and biases them all in favor of A
        A = new Thread() {
            @Override
            public void run() {
                for (int i = 0; i < 40; i++) {
                    User a = new User();
                    (a);
                    //After acquiring the locks, they all become biased locks, favoring thread A
                    synchronized (a) {

                    }
                }
                //wake up thread B
                (B); }
            }
        }; }

        //Thread B revokes the first 20 biased locks to reach the bulk rebiasing threshold
        // Rebias the last 20 biased locks to thread B
        B = new Thread() {
            B = new Thread() { @Override
            public void run() {
                //Wait for thread A to wake up
                ();
                for (int i = 0; i < 40; i++) {
                    User a = (i); //Take all the favorites from the list.
                    //Take out of the list in favor of thread A.
                    ("B {}th time before lock" + (a).toPrintable(), i + 1); synchronized (a) {
                    synchronized (a) {
                        // the first 19 times the bias lock is revoked to favor thread A, and then the lightweight lock is upgraded to point to the lock record on thread B's thread stack
                        // the 20th time the bias lock is rebiased in favor of thread B
                        ("B locks in {}th time" + (a).toPrintable(), i + 1);
                    }
                    //because the first 19 times are lightweight locks, and are lockless unbiased after release
                    // but the 20th time is a biased lock biased towards thread B still biased towards thread B after release
                    ("B locking ends {}th time" + (a).toPrintable(), i + 1);
                }
                // After the 20th rebiasing, the epoch field of the User class becomes 1, and the epoch field in the newly spawned object is 1 as well.
                ("B Newly spawned object:" + (new User()).toPrintable());
                //wake up thread C
                (C);
            }

        }.

        //Thread C performs bias lock revocation for the last 20 bias locks that are biased to thread B.
        //adding the 20 revocations by thread B makes a total of 40 revocations, reaching the threshold for bulk revocation of bias locks
        C = new Thread() {
            C = new Thread() { @Override
            public void run() {
                // wait for thread B to wake up
                ();
                //The list array is a lightweight lock up to coordinates 20.
                //20 and beyond are biased locks in favor of thread B, so start at coordinate 20
                for (int i = 20; i < 40; i++) {
                    User a = (i);
                    // All that's taken out here are biased locks in favor of thread B
                    ("C {}th time before locking" + (a).toPrintable(), i - 20 + 1);
                    synchronized (a) {
                        // Since the epochs are the same at 1, it's all upgraded to a lightweight lock
                        ("C locking in the {}th time" + (a).toPrintable(), i - 20 + 1);
                    }
                    // After the lightweight lock is released, it all becomes unbiased and lock-free
                    ("C locking ended {}th time" + (a).toPrintable(), i - 20 + 1);
                    // Observe if a bulk undo occurs at this point, if it does, the new object will be lock-free
                    ("C Generate new object for {}th time: {}", i - 20 + 1, (new User()).toPrintable());
                }
                // 20 more biased lock revocations occur in the C thread, reaching the bulk revocation threshold
                //At this point the created objects should all be in a lock-free state
                ("C Newly spawned objects: " + (new User()).toPrintable()); }
            }

        }
        ().
        ().
        ().
        ().
    }

}

The results of the run are as follows

Let's first look at the results of thread B's twenty previous runs, where the bias locks are revoked from the 1st element to the 19th, the locks are upgraded to lightweight locks, and the locks are released to a lockless state

image-20241023154629522

Thread B reaches the bulk rebiased threshold when it acquires the 20th biased lock, and it is different from the first 19 times, where bulk rebiasing occurs and the thread maintains the biased lock state and is biased in favor of Thread B; this is also true for the following 20 through 40

image-20241023160657936

Then look at thread C, thread C starts fetching elements from the 21st element, the elements taken out must all be biased toward thread B's biased locks, and thread C's attempts to acquire these locks will result in the locks being upgraded to lightweight locks, and the outputs from 21st to 39th are similar to the outputs from 39th, and 39th as an example of the outputs are as follows

image-20241023161648421

Next is the key point, because thread C acquires the 40th bias lock in the list for the 20th time, and together with the 20 revocations by thread B, it will reach 40, which is the threshold for bias lock revocation, and if nothing happens, after this time, there will be a bulk revocation of bias locks, and then the object will be created, and it will be in a lockless state.

image-20241023162246144

Sure enough, a bulk undo occurs, proving that undoing 40 times triggers a bulk undo, and the re-created object will become lock-free, at which point the class's bias locking is effectively completely disabled.

6. Bulk withdrawal of cooling-off period

Discussing another parameter about biased locking: BiasedLockingDecayTime here, the

parameters an explanation of the meaning of words or phrases
BiasedLockingDecayTime=25000 Batch Undo Time Threshold

From the output, it defaults to 25s

[root@lenovo ~]# java -XX:+PrintFlagsFinal | grep BiasedLock
     intx BiasedLockingBulkRebiasThreshold          = 20                                  {product}
     intx BiasedLockingBulkRevokeThreshold          = 40                                  {product}
     intx BiasedLockingDecayTime                    = 25000                               {product}
     intx BiasedLockingStartupDelay                 = 4000                                {product}
     bool TraceBiasedLocking                        = false                               {product}
     bool UseBiasedLocking                          = true                                {product}

This parameter means

  1. A batch revocation occurs if it occurs within 25 seconds from the last batch rebias and the cumulative revocation count reaches 40 (the bias locking feature of the class is completely disabled)
  2. If it is reset outside of more than 25 seconds since the last batch re-bias occurred in the[20, 40) I'll give it another chance.

This thing I gave him a name called "undo cooling-off period", in the cooling-off period do not try to get lock bias lock undo, then after the cooling-off period will give you 1 to 20 times the opportunity to undo, the equivalent of the bulk undo threshold increased.

This can be verified by adding a delay action to the batch undo demo code in the previous section. To reduce the wait time, you can add JVM startup parameters:-XX:BiasedLockingDecayTime=5000 Change the batch undo time threshold to 5 seconds. Stop the thread for 6 seconds at the 20th time of the C-thread loop

C = new Thread() {
            public void run() {
            public void run() {
                ();
                //List arrays up to coordinate 20 are lightweight locks
                //20 and beyond are biased locks in favor of thread B, so start at coordinate 20
                for (int i = 20; i < 40; i++) {
                    //TODO try stopping the thread for 6 seconds, 5 seconds, 4.9 seconds and observe the final output
                    if (i == 39) {
                        try {
                            (4900); }
                        } catch (InterruptedException e) {
                            (); } catch (InterruptedException e) {
                        }
                    }
                    User a = (i);
                    // All that is taken out here is a biased lock in favor of thread B
                    ("C {}th time before locking" + (a).toPrintable(), i - 20 + 1);
                    synchronized (a) {
                        // Since the epochs are the same at 1, it's all upgraded to a lightweight lock
                        ("C locking in the {}th time" + (a).toPrintable(), i - 20 + 1);
                    }
                    // After the lightweight lock is released, it all becomes unbiased and lock-free
                    ("C locking ended {}th time" + (a).toPrintable(), i - 20 + 1);
                    // Observe if a bulk undo occurs at this point, if it does, the new object will be lock-free
                    ("C Generate new object for {}th time: {}", i - 20 + 1, (new User()).toPrintable());
                }
                // 20 more biased lock revocations occur in the C thread, reaching the bulk revocation threshold
                //At this point the created objects should all be in a lock-free state
                ("C Newly spawned objects: " + (new User()).toPrintable()); }
            }

        };

It will be noticed that after pausing for 6 seconds and 5 seconds, the new objects eventually created by C threads are in a biasable state, whereas after pausing for 4.9 seconds, the threads eventually created by C threads become in a non-biasable and lock-free state.

7、Flow chart

The bias lock is a very complex lock type, and due to its complexity, it has been deprecated (note deprecated, not removed) in JDK15 and later versions. Of course, you can post whatever you want, I'm using java8, hehehe. Anyway, mastered better than not mastered, according to the understanding, I drew a flow chart to show the transformation process of the bias lock lock state, this chart should have errors, if there are errors, please leave a message, I'll correct it ~!

无锁、偏向锁、轻量级锁、重量级锁-偏向锁转化流程图.drawio

IV. Lightweight locks

The main purpose of introducing lightweight locks is to reduce the performance loss generated by heavyweight locks by competing for the locks through the CAS mechanism when the multi-thread competition is not intense. Heavyweight locks use the operating system's underlying mutex lock (Mutex Lock), which causes threads to switch frequently between the user state and the core state, resulting in a large performance loss.

1, lightweight lock core principle

Lightweight locking exists to minimize the use of mutex locks at the operating system level because of their poor performance. Blocking and waking up of threads requires the CPU to switch from the user state to the kernel state, and frequent blocking and waking up is a very burdensome task for the CPU. At the same time, we can find that the locking state of many object locks will only last for a short period of time, such as the self-add operation of integers, blocking and waking up the thread in a short period of time is obviously not worth it, for this reason, the introduction of lightweight locks. Lightweight locks are a kind of spin locks, because the JVM itself is an application, so hope that at the application level through the spin to solve the thread synchronization problem.

The execution process of lightweight locks: before the lock-grabbing thread enters the critical zone, if the built-in lock (the synchronization object in the critical zone) is not locked, the JVM will first create in the lock-grabbing thread's stack frame aLock RecordThis is used to store a copy of the object's current Mark Word, at which point the thread stack and the built-in lock object header look roughly as follows

image-20241024141506721

Then the lock-grabbing thread will use the CAS spin operation to try to update the ptr_to_lock_record of the Mark Word in the header of the built-in lock object to the address of the lock record in the lock-grabbing thread's stack frame, and if this update is performed successfully, the thread owns the object lock. Then the JVM changes the lock flag bit in the Mark Word to 00 (lightweight lock flag), which means that the object is in a lightweight lock state. After the lock is successfully grabbed, the JVM will save the original lock object information (such as hash code, etc.) in the Mark Word in the lock record of the thread to grab the lock in the Displaced Mark Word (can be interpreted as a misplaced Mark Word) field, and then the lock record in the lock thread to point to the lock object of the owner pointer.

After a successful lightweight lock seizure, the state of the lock record and the object header is shown in the following diagram

轻量级锁结果图示

2、Lightweight lock case

The case for lightweight locks was already mentioned in the previous section, so here it is again

import ;
import ;
import ;
import .slf4j.Slf4j;
import ;


/**
 * @author kdyzm
 * @date 2024/10/18
 * At startup, be sure to add theJVMpriming parameter:-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0
 */
@Slf4j
public class BiasedLockTest {

    public static void main(String[] args) throws InterruptedException {
        User lock = new User();
        Thread threadA = runThread(lock, "A");
        Thread threadB = runThread(lock, "B");
        ();
        ();
        ();
        ();
    }

    private static Thread runThread(User lock, String name) throws InterruptedException {
        Thread thread = new Thread(() -> {
            ("Grab the front of the lock.lockpresent state:\n{}", ());
            for (int i = 0; i < 100; i++) {
                synchronized (lock) {
                    if (i == 99) {
                        ("possessor's locklockpresent state:\n{}", ());
                    }
                }
            }
            ("After releasing the locklockpresent state:\n{}", ());
        }, name);
        return thread;
    }

    @ToString
    @Setter
    @Getter
    public static class User {
        private String userName;

        //JOLObject Structure String
        public String getObjectStruct() {
            return (this).toPrintable();
        }
    }
}

Thread A execution result:

image-20241021221902252

Thread B execution result:

image-20241021224745019

You can see that thread B waited until thread A finished to acquire the lock lock, and by acquiring the lock lock again, the lock becomes a lightweight lock, which is released to a lockless state. As explained from the previous section, the reason why rebiasing didn't happen is because batch rebiasing needs to reach the threshold of the number of times a batch rebiasing can be undone, which is 20 by default, and before that, any new thread attempting to acquire the lock will result in the biased lock being upgraded to a lightweight lock.

3、Lightweight lock classification

Lightweight locks are essentially spin locks, so-called "spin" is essentially a loop retry, which can be of two types:Ordinary spin lockcap (a poem)Adaptive spin lock

Ordinary spin lock

By ordinary spin locks, we mean that when a thread comes to compete for the lock, the robbing thread waits in place in a loop, instead of being blocked, until the thread in possession of the lock releases it, and then the robbing thread can acquire the lock.

Locks consume CPU while waiting in a loop in place, which is equivalent to executing an empty loop that does nothing. So lightweight locks are suitable for scenarios where the critical area code takes a short time, so that the thread can acquire the lock while waiting in place for a short time.

In JDK 1.6, the Java Virtual Machine provides the -XX:+UseSpinning parameter to turn on spin locking. By default, the number of spins is 10, and the number of times to wait for spin locking is set using the -XX:PreBlockSpin parameter.

In versions of the JDK after 1.7, the parameters for spinlocks were removed and the virtual machine no longer supports spinlocks that are configured by the user. Spin locks are always executed and the spin count is adjusted by the VM itself.

Adaptive spin lock

The so-called adaptive spin lock, is waiting for the thread empty loop is not a fixed number of spin, but will dynamically change the number of spin waiting according to the actual situation, the number of spins by the previous spin time on the same lock and the state of the lock owner to determine. The approximate principle of adaptive spin locks is:

(1) If a lock-grabbing thread has previously succeeded in acquiring a lock on the same lock object, the JVM assumes that there is a high probability that the spin will succeed again, and therefore allows the spin wait to continue for a relatively long period of time.

(2) If a lock has rarely been successfully acquired by a lock-grabbing thread, the JVM will likely reduce the spin time or even omit the spin process to avoid wasting processor resources.

Adaptive spin solves the problem of "uncertain lock contention time". Adaptive spin assumes that different threads hold the same lock object for roughly the same amount of time, and that the degree of competition stabilizes. The general idea is: according to the time and results of the last spin to adjust the time of the next spin.

4、Flow chart

Here's a flowchart of how an object in a lockless state becomes a lightweight lock

image-20241024164546231

V. Heavyweight locks

Heavyweight locks and bias locks, lightweight locks are different, bias locks, lightweight locks are essentially optimistic locks, they are application-level locks (JVM itself is an application), heavyweight locks based on the operating system kernel's mutex locks to achieve the user and kernel state switching occurs, overhead to be larger, so it is called "heavyweight locks This is why they are called "heavyweight locks".

1, heavyweight lock core principle

As mentioned before, about the synchronized keyword underlying the use of monitor locks, each object in the JVM will have a monitor, the monitor and the object together with the creation and destruction. The monitor is equivalent to a special room used to monitor the entry of these threads, and its obligation is to ensure that (at the same time) only one thread can access the protected critical area code block.

Essentially, a monitor is a synchronization tool, or a synchronization mechanism, if you will, with key features:

(1) Synchronization. The critical zone code protected by monitors is executed mutually exclusive. A monitor is a license to run, and any thread that enters the critical zone code needs to obtain this license and return it when it leaves.

(2) Collaboration. The monitor provides Signal mechanism, allowing the thread that is holding a license to temporarily give up the license to enter the blocking state, waiting for other threads to send Signal to wake up; other threads with a license can send Signal to wake up the thread that is blocking the waiting thread, so that it can regain the license and start execution.

In the Hotspot virtual machine, the monitor is implemented by the C++ class ObjectMonitor, which is defined in theshare/vm/runtime/file, the constructor code is roughly as follows:

//Monitor structure
 ObjectMonitor::ObjectMonitor() {
   _header = NULL;
   _count = 0.
   _waiters = 0, _count = 0; _count = 0

   // Number of thread reentries
   _recursions = 0; _object = NULL; //thread reentries
   _object = NULL.

//Identifies the thread that owns the Monitor
   _owner = NULL;

  // A bi-directional circular chain of waiting threads.
   _WaitSet = NULL; //Waiting threads form a two-way circular chain.
   _WaitSetLock = 0 ;
   _Responsible = NULL ;
   _succ = NULL ;

  // Unidirectional linked list when multiple threads compete for lock entry
   cxq = NULL ;
   FreeNext = NULL ;

  //_owner wakes up the thread node from this bidirectional circular link list
   _EntryList = NULL ;
   _SpinFreq = 0 ;
   _SpinClock = 0 ;
   OwnerIsThread = 0 ;
 }

The ObjectMonitor's Owner (_owner), WaitSet (_WaitSet), Cxq (_cxq), and EntryList (_EntryList) attributes are more critical.The ObjectMonitor's WaitSet, Cxq, and EntryList queues hold the thread that threads that grab heavyweight locks, and the thread pointed to by ObjectMonitor's Owner is the thread that acquires the lock.

The three queues, Cxq, EntryList, and WaitSet, are described below:

(1) Cxq: Contention Queue, all threads requesting locks are first placed in this contention queue.

(2) EntryList: those threads in Cxq that qualify as candidate resources are moved to the EntryList.

(3) WaitSet: a thread that has an ObjectMonitor will be blocked after calling the () method, and then the thread will be placed in the WaitSet chain.

The internal lock grabbing process is as follows

image-20241025144330190

Designing the underlying C++ line of sight, the code is a bit more complex, if you are interested, you can refer to the article:

Java multithreading: objectMonito source code interpretation

2. Heavyweight lock case

The following self-incrementing case shows the progression from no locks, to biased locks, to lightweight locks, and eventually expanding to heavyweight locks.

import .slf4j.
import ;

import ;

/**
 * @author kdyzm
 * @date 2024/10/25
 * Be careful to add JVM startup parameters before running: -XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0 -XX:BiasedLockingDecayTime=5000
 */
@Slf4j
public class FatLock {

    private static final FatLock LOCK = new FatLock();

    

    public static void main(String[] args) throws InterruptedException {
        // Thread A performs 100 self-increments
        Thread A = new Thread(() -> {
            ("Before locking: {}", (LOCK).toPrintable());
            for (int i = 0; i < 100; i++) {
                synchronized (LOCK) {
                    counter++; if (i == 99) { synchronized (LOCK) {
                    if (i == 99) {
                        ("Locked in: {}", (LOCK).toPrintable());
                    }
                }
            }
            ("After locking: {}", (LOCK).toPrintable()); }
        }, "A");
        // Thread B performs 100 self-additions and enters the wait state at the 100th time
        Thread B = new Thread(() -> {
            ("Before locking: {}", (LOCK).toPrintable());
            for (int i = 0; i < 100; i++) {
                synchronized (LOCK) {
                    counter++; if (i == 99) { synchronized (LOCK) {
                    if (i == 99) {
                        ("Locking in: {}", (LOCK).toPrintable());
                        // enter wait state to create heavyweight lock formation condition
                        ();
                    }
                }
            }
            ("After locking: {}", (LOCK).toPrintable());
        }, "B");
        // Thread C performs 100 self-increments
        Thread C = new Thread(() -> {
            ("Before locking: {}", (LOCK).toPrintable());
            for (int i = 0; i < 100; i++) {
                synchronized (LOCK) {
                    counter++; if (i == 99) { synchronized (LOCK) {
                    if (i == 99) {
                        ("Locking in: {}", (LOCK).toPrintable());
                    }
                }
            }
            ("After locking: {}", (LOCK).toPrintable()); }
        }, "C");

        ();
        // Wait for thread A to finish executing
        (1000);
        ().
        //Wait for thread B to finish executing and enter the wait state
        (1000); (); //Wait for thread B to finish executing and enter the wait state.
        ().
        //Wait for thread C to enter the blocking state.
        (1000); //Wait for thread B to finish execution and enter the waiting state.
        //resume the running state of thread B
        (B);
    }
}

The results of the run are as follows

thread name pre-locked Locked. locked
A biasable (biasable, no bias thread id set) biased (already biased A thread) biased (already biased A thread)
B biased (already biased A thread) thin lock fat lock
C thin lock fat lock non-biasable (no locks)

The flow chart is shown below

无锁、偏向锁、轻量级锁、重量级锁-重量级锁升级.drawio

VI. Reference documents

JAVA High Concurrency Core Programming Volume 2: Multithreading, Locks, JMM, JUC, High Concurrency Design Patterns.

Bias Lock - Bulk Rebias and Bulk Undo Tests

In-depth bias lock

Deeper into JVM locks

How many of the bias locks and weighted locks can you catch in multiple combinations?

Java Multithreading: objectMonitor source code interpretation (3)

Biased locks, lightweight locks, heavyweight locks, the ultimate Synchronized underlying source code analysis!



Finally, feel free to follow my blog: ~