Overview of several states of JVM built-in lock synchronized

Posted by ChaosXero on Mon, 24 Jan 2022 01:10:43 +0100

Use of built-in lock

Generally, the java built-in lock we mentioned refers to the lock implemented by the synchronized keyword provided by the JVM. Here is a simple example:

public class SynchronizedVariableTest1 {

    public static void main(String[] args) throws InterruptedException {
        SynchronizedVariableTest1 test = new SynchronizedVariableTest1();
        synchronized (test) {
            System.out.println(1);
        }
    }
}
Copy code

Object locking

We can view bytecode files through javap commands, or view bytecode instruction information through jclasslib Bytecode Viewer plug-in of idea, as shown in the following figure:

We can see that the bottom layer of synchronized uses the monitor mechanism to obtain and release locks. The monitorenter and monitorexit instructions will be added before and after the code is fast. ​

**The locked object cannot be null. If it is null, the program will prompt * * * * NullPointerException * * null pointer exception when running.

Object lock = null;
synchronized(lock) {
    System.out.println(100);
}

// result: 
// Exception in thread "main" java.lang.NullPointerException
//	at cn.xyz.juc.synchronized1.status.CleanLockTest.main(CleanLockTest.java:16)
Copy code

Method locking

Similarly, if the synchronized keyword is added to the method (because it is not convenient for the jclasslib Bytecode Viewer to view the method information, I will demonstrate it through the javap -verbose instruction below), ACC will be added to the flags of the method_ Synchronized keyword. The original method code is as follows:

public synchronized void test() {
}
Copy code

The compiled bytecode executes the instruction javap - verbose XXXX class

  public synchronized void test();
    descriptor: ()V
    flags: (0x0021) ACC_PUBLIC, ACC_SYNCHRONIZED
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: iconst_1
         4: invokevirtual #6                  // Method java/io/PrintStream.println:(I)V
        line 14: 21
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      22     0  this   Lcn/xyz/juc/synchronized1/SynchronizedVariableTest2;

Copy code

For For parsing and analysis of class bytecode files, please refer to this Nuggets article: JVM bytecode instruction parsing . ​

Built in lock status storage

The state of the built-in lock is stored in the object header of the Java object. For the storage structure of the object header and the object memory allocation, please refer to my article: Uncover the secrets of Java objects based on Hostpot virtual machine . This article will not repeat.

Mark Word (64bit)

In order to facilitate the following reading, I will paste mark word into this article again

Java tube pass

Java virtual machine can support method level synchronization and synchronization of an instruction sequence within a method. Both synchronization structures are implemented by Monitor (more commonly referred to as "lock").

java adopts the management process technology. The synchronized keyword and the three methods of wait(), notify(), and notifyAll() are all components of the management process. The pipe pass and semaphore are equivalent. The so-called equivalence refers to that the semaphore can be realized by the pipe pass, and the pipe pass can also be realized by the semaphore. However, the management process uses the encapsulation characteristics of OOP to solve the complexity of semaphores in engineering practice, so java adopts the management mechanism.

MESA model

Pipe pass model: Hassen model, Hoare model and MESA model. Among them, the MESA model is widely used now, and the implementation of Java management also refers to the MESA model.

In the field of concurrent programming, there are two core problems: one is mutual exclusion, that is, only one thread is allowed to access shared resources at the same time; The other is synchronization, that is, how threads communicate and cooperate. These two problems can be solved by the tube side.

ObjectMonitor

The definition information of ObjectMonitor in the jvm is as follows:

  // initialize the monitor, exception the semaphore, all other fields
  // are simple integers or pointers
  ObjectMonitor() {
    _header       = NULL;      // Object header
    _count        = 0;
    _waiters      = 0,
    _recursions   = 0;         // Lock reentry times   
    _object       = NULL;      // Store lock object
    _owner        = NULL;      // Identifies the thread that owns the monitor (the thread that currently acquires the lock) 
    _WaitSet      = NULL;      // A two-way circular linked list composed of waiting threads (calling waite)_ WaitSet is the first node
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;      
    _succ         = NULL ;
    _cxq          = NULL ;     // The multi-threaded contention lock will be stored in the one-way linked list (FIFO) structure first
    FreeNext      = NULL ;     // Store threads that are Blocked when entering or re entering (i.e. threads that fail to compete)
    _EntryList    = NULL ;
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
    _previous_owner_tid = 0;
  }
Copy code

Built in lock status

The built-in lock is divided into four states: no lock, bias lock, lightweight lock and heavyweight lock. In the process of lock competition, a positive lock upgrade process will be carried out.

Lock upgrade process

Note: the lock upgrade status is irreversible.

Unlocked state

Experiment code:

public class NoSynchronizedTest {

    public static void main(String[] args) {
        NoSynchronizedTest test = new NoSynchronizedTest();
        System.out.println("Unlocked state +++++++++");
        System.out.println(ClassLayout.parseInstance(test).toPrintable());
    }
}
Copy code

Output results

Unlocked state +++++++++
cn.xyz.juc.synchronized1.NoSynchronizedTest object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           05 c0 00 f8 (00000101 11000000 00000000 11111000) (-134168571)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
 Copy code

Field interpretation:

  • OFFSET: address OFFSET, in bytes;
  • SIZE: occupied memory SIZE, in bytes;
  • TYPE DESCRIPTION: TYPE DESCRIPTION, where object header is the object header;
  • VALUE: corresponding to the VALUE currently stored in memory, binary 32;

Pointer compression

As a result of printing, we can see that the total size of the object is 16 bytes. The first 12 bytes are the object header (pointer compression is enabled by default in my local jdk 1.8), and the last 4 bytes are aligned filling. ​

It can be closed through the following parameters:

-XX:-UseCompressedOops
 Copy code

I'll run it again. The total size of the object is 16 bytes. The first 8 bytes are mark word, and the last 8 bytes represent kclass point

Anonymity bias

When the JVM enables the skew lock mode (JDK 6 is enabled by default), the Thread Id of the new Mark Word created is 0, indicating that it is in a skewable but not biased to any thread, which is also called anonymous biased

Bias lock state

Bias lock delay bias

**There is a bias lock delay mechanism in the bias lock mode: * * after the Hostpost virtual machine is restarted, there is a delay of a few seconds (4 seconds by default) before starting the bias lock mode for new objects. When the JVM starts, a series of object creation processes are performed. In this process, a large number of synchronized keywords lock objects. Most of these locks are not biased locks. To reduce initialization time, the JVM delays loading biased locks by default. ​

JVM parameters:

//Closing delay opening bias lock 
‐XX:BiasedLockingStartupDelay=0 

//No deflection lock
‐XX:‐UseBiasedLocking

//Enable deflection lock
‐XX:+UseBiasedLocking
 Copy code

Test code (note that it should be noted here that two objects need to be created, because the initialization of object header information is initialized when the new keyword is executed. The author has encountered such a problem before, resulting in the failure of the experiment):

public class BiasedLockDelayTest {

    public static void main(String[] args) throws InterruptedException {
        Object obj1 = new Object();
        System.out.println(ClassLayout.parseInstance(obj1).toPrintable());
        System.out.println();
        Thread.sleep(5000);
        Object obj2 = new Object();
        System.out.println(ClassLayout.parseInstance(obj2).toPrintable());
    }
}
Copy code

The results are printed as follows. After a delay of 5 seconds, the anonymous bias is turned on by default, and threadid = 0.

Bias lock bias state tracking

The following code mainly demonstrates the process of an object from no lock to biased lock. The code is as follows:

log.debug(ClassLayout.parseInstance(new Object()).toPrintable());
Thread.sleep(4000);
Object obj1 = new Object();
log.debug(ClassLayout.parseInstance(obj1).toPrintable());
new Thread(new Runnable() {
    @Override
    public void run() {
        log.debug("start ... \r\n" + ClassLayout.parseInstance(obj1).toPrintable());
        synchronized (obj1) {
            log.debug("lock ... \r\n" + ClassLayout.parseInstance(obj1).toPrintable());
        }
        log.debug("unlock ... \r\n" + ClassLayout.parseInstance(obj1).toPrintable());
    }
}, "thread0").start();

Thread.sleep(5000);
log.debug(ClassLayout.parseInstance(obj1).toPrintable());
Copy code

The printing results are as follows:

The hashcode method is called when the lock state is biased

Call obj. Of lock pair redemption Hashcode () or system When using the identityhashcode (obj) method, the bias lock of the object will be revoked. Because an object's hashcode will only be generated once and saved, there is no place to store the hashcode

  • Lightweight locks record hashCode in the lock record
  • The heavyweight lock records the hashCode in the Monitor

When the object is biased (that is, the thread ID is 0) and biased, calling hashCode calculation will make the object unable to be biased:

  • When the object can be biased, MarkWord will program an unlocked state and can only be upgraded to a lightweight lock
  • When the object is in a biased lock, call hashCode to force the biased lock to be upgraded to a heavyweight lock

Experiment code:

The hashCode method of the shared object obj1 is called inside the lock, and the lock is upgraded to a heavyweight lock.

Call wait/notify in the lock state

Execute obj in the bias lock state Notify will be upgraded to a lightweight lock.

Call obj Wait (timeout) will be upgraded to heavyweight lock.

Lightweight lock status

If the bias lock fails, the virtual machine will not be upgraded to a heavyweight lock. It will also try to use an optimization method called lightweight lock. At this time, the structure of Mark Word will also become a lightweight lock. The scenario of lightweight lock is that threads alternately execute synchronization blocks. If there are occasions where multiple threads access the same lock at the same time, This will cause the lightweight lock to expand into a heavyweight lock.

Upgrade bias lock to lightweight lock

@Slf4j
public class LightweightLockTest {

    public static void main(String[] args) throws InterruptedException {
        log.debug(ClassLayout.parseInstance(new Object()).toPrintable());
        Thread.sleep(5000);
        Object obj1 = new Object();
        log.debug(ClassLayout.parseInstance(obj1).toPrintable());
        new Thread(new Runnable() {
            @SneakyThrows
            @Override
            public void run() {
                log.debug("start ... \r\n" + ClassLayout.parseInstance(obj1).toPrintable());
                synchronized (obj1) {
                    log.debug("lock ... \r\n" + ClassLayout.parseInstance(obj1).toPrintable());
                }
                log.debug("unlock ... \r\n" + ClassLayout.parseInstance(obj1).toPrintable());
            }
        }, "thread0").start();

        Thread.sleep(1000);

        new Thread(new Runnable() {
            @SneakyThrows
            @Override
            public void run() {
                log.debug("start ... \r\n" + ClassLayout.parseInstance(obj1).toPrintable());
                synchronized (obj1) {
                    log.debug("lock ... \r\n" + ClassLayout.parseInstance(obj1).toPrintable());
                }
                log.debug("unlock ... \r\n" + ClassLayout.parseInstance(obj1).toPrintable());
            }
        }, "thread1").start();

        Thread.sleep(5000);
        log.debug(ClassLayout.parseInstance(obj1).toPrintable());
    }
}
Copy code

Print log:

Because the log is too long to be marked, I'll simply draw a flow chart

Upgrade from lightweight lock to heavyweight lock

To create a highly competitive scenario, we can simulate it through thread pool. The code is as follows:

@Slf4j
public class LightweightLockTest {

    public static void main(String[] args) throws InterruptedException {
        log.debug(ClassLayout.parseInstance(new Object()).toPrintable());
        Thread.sleep(5000);
        Object obj1 = new Object();
        log.debug(ClassLayout.parseInstance(obj1).toPrintable());

        ExecutorService executeService = Executors.newFixedThreadPool(2);
        executeService.submit(new Thread(new Runnable() {
            @SneakyThrows
            @Override
            public void run() {
                log.debug("start ... \r\n" + ClassLayout.parseInstance(obj1).toPrintable());
                synchronized (obj1) {
                    log.debug("lock ... \r\n" + ClassLayout.parseInstance(obj1).toPrintable());
                }
                log.debug("unlock ... \r\n" + ClassLayout.parseInstance(obj1).toPrintable());
            }
        }, "thread0"));

        //Thread.sleep(1000);

        executeService.submit(new Thread(new Runnable() {
            @SneakyThrows
            @Override
            public void run() {
                log.debug("start ... \r\n" + ClassLayout.parseInstance(obj1).toPrintable());
                synchronized (obj1) {
                    log.debug("lock ... \r\n" + ClassLayout.parseInstance(obj1).toPrintable());
                }
                log.debug("unlock ... \r\n" + ClassLayout.parseInstance(obj1).toPrintable());
            }
        }, "thread1"));

        Thread.sleep(5000);
        log.debug(ClassLayout.parseInstance(obj1).toPrintable());
    }
}
Copy code

Let's look at the result again: after the first thread obtains the lock, the lock is upgraded from biased lock to lightweight lock

The second thread has adopted heavyweight locks. Note: why not upgrade to heavyweight locks sometimes? This may be because the CPU resources are relatively idle, the computing logic processing capacity is relatively strong, and there is no need to upgrade the lock. You can try to add several more threads to compete for the lock, or run the program more times.

Lock state transition

The state transformation of bias lock and lightweight lock and the relationship transformation of object Mark Word are shown in the following figure:

Original link: https://juejin.cn/post/7055665818176061453
 

Topics: Java intellij-idea