In-depth understanding of the synchronized implementation principle of Java concurrency

Recently, I have been sorting out some knowledge about synchronization, and I have found that the ocean of knowledge is boundless and the learning is endless. Here, I will use the article I thought to sort out the underlying implementation principle of synchronization.

First of all, what are the application scenarios of synchronized?
In summary, process synchronization:

  1. Modified instance method, which acts on the current instance to lock, and obtains the lock of the current instance before entering the synchronization code

  2. Modify the static method, which acts on the lock of the current class object, and obtains the lock of the current class object before entering the synchronization code

  3. Modify the code block, specify the lock object, lock the given object, and obtain the lock of the given object before entering the synchronization code base. Specifically, there are data structure implementations such as hashtable.

So, how is the bottom layer of synchronized implemented?

Now we redefine a synchronized modified synchronized code block to operate the shared variable i in the code block, as follows

public class SyncCodeBlock {

   public int i;

   public void syncTask(){
       //同步代码库
       synchronized (this){
           i++;
       }
   }
}

After compiling the above code and decompiling with javap, the bytecode is as follows (here we omit some unnecessary information):

Classfile /Users/zejian/Downloads/Java8_Action/src/main/java/com/zejian/concurrencys/SyncCodeBlock.class
  Last modified 2017-6-2; size 426 bytes
  MD5 checksum c80bc322c87b312de760942820b4fed5
  Compiled from "SyncCodeBlock.java"
public class com.zejian.concurrencys.SyncCodeBlock
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
  //........省略常量池中数据
  //构造函数
  public com.zejian.concurrencys.SyncCodeBlock();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 7: 0
  //===========主要看看syncTask方法实现================
  public void syncTask();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=3, locals=3, args_size=1
         0: aload_0
         1: dup
         2: astore_1
         3: monitorenter  //注意此处,进入同步方法
         4: aload_0
         5: dup
         6: getfield      #2             // Field i:I
         9: iconst_1
        10: iadd
        11: putfield      #2            // Field i:I
        14: aload_1
        15: monitorexit   //注意此处,退出同步方法
        16: goto          24
        19: astore_2
        20: aload_1
        21: monitorexit //注意此处,退出同步方法
        22: aload_2
        23: athrow
        24: return
      Exception table:
      //省略其他字节码.......
}
SourceFile: "SyncCodeBlock.java"

We mainly focus on the following code in the bytecode:

3: monitorenter  //进入同步方法
//..........省略其他  
15: monitorexit   //退出同步方法
16: goto          24
//省略其他.......
21: monitorexit //退出同步方法

It can be seen from the bytecode that the implementation of the synchronization statement block uses the monitorenter and monitorexit instructions, where the monitorenter instruction points to the start position of the synchronization code block, and the monitorexit instruction indicates the end position of the synchronization code block. When the monitorenter instruction is executed, the current thread will Attempting to obtain the ownership of the monitor corresponding to the objectref (ie, the object lock), when the entry counter of the monitor of the objectref is 0, the thread can successfully obtain the monitor and set the counter value to 1, and the lock is successfully obtained. If the current thread already owns the monitor of objectref, it can re-enter the monitor (re-entrancy will be analyzed later), and the value of the counter will be incremented by 1 when re-entrant. If other threads already have the ownership of the monitor of objectref, the current thread will be blocked until the executing thread is executed, that is, the monitorexit instruction is executed, the executing thread will release the monitor (lock) and set the counter value to 0, and other threads will have Chance to hold monitor. It is worth noting that the compiler will ensure that no matter how the method is completed, each monitorenter instruction called in the method executes its corresponding monitorexit instruction, regardless of whether the method ended normally or abnormally. In order to ensure that the monitorenter and monitorexit instructions can still be correctly paired and executed when the method is abnormally completed, the compiler will automatically generate an exception handler, which declares that it can handle all exceptions, and its purpose is to execute the monitorexit instruction. It can also be seen from the bytecode that there is an additional monitorexit instruction, which is the instruction to release the monitor that is executed at the end of an abnormality.

There is a monitor/monitor lock here, so what is the semantic principle of synchronized?
Here, the structure of the object in memory is described first, so that the synchronized implementation can be explained later:

The synchronization (synchronized) in the Java virtual machine is implemented based on the entry and exit of the monitor (Monitor) object, whether it is explicit synchronization (with explicit monitorenter and monitorexit instructions, that is, synchronization code blocks) or implicit synchronization. In the Java language, the place where synchronization is used the most is probably the synchronized method modified by synchronized. The synchronization method is not implemented by the monitorenter and monitorexit instructions, but implicitly implemented by the method call instruction reading the ACC_SYNCHRONIZED flag of the method in the runtime constant pool. This point will be analyzed in detail later. Let's first understand a concept Java object header, which is very critical to deeply understand the implementation principle of synchronized.

Understanding Java Object Header and Monitor

In the JVM, the layout of objects in memory is divided into three areas: object header, instance data and alignment padding. as follows:
write picture description here

Instance variable : stores the attribute data information of the class, including the attribute information of the parent class. If the instance part of the array also includes the length of the array, this part of the memory is aligned by 4 bytes.

Padding data : Since the virtual machine requires that the starting address of the object must be an integer multiple of 8 bytes. Padding data does not have to exist, just for byte alignment, you can understand this.

For the top, it is the Java header object, which implements the basis of the synchronized lock object. We focus on analyzing it. Generally speaking, the lock object used by synchronized is stored in the Java object header, and 2 words are used in the jvm. To store the object header (if the object is an array, it will allocate 3 words, and the extra word records the length of the array). Its main structure is composed of Mark Word and Class Metadata Address, and its structure is described in the following table:
For The top is the Java header object, which implements the basis of the synchronized lock object. We focus on analyzing it. Generally speaking, the lock object used by synchronized is stored in the Java object header, and 2 words are used in the jvm to store The object header (if the object is an array, it will allocate 3 words, and the extra word records the length of the array). Its main structure is composed of Mark Word and Class Metadata Address. The structure is described in the following table:

! Virtual machine bit header object structure description
32/64bit Mark Word Stores the object's hashCode, lock information or generation age or GC flag and other information
32/64bit Class Metadata Address Type pointer points to the object's class metadata, which is determined by the JVM Which class the object is an instance of.

Among them, Mark Word stores the object's HashCode, generation age, lock flag bit, etc. by default. The following is the default storage structure of Mark Word for 32-bit JVM

write picture description here
Since the information of the object header is an additional storage cost that has nothing to do with the data defined by the object itself, considering the space efficiency of the JVM, Mark Word is designed to be a non-fixed data structure in order to store more effective data. The state of the object itself reuses its own storage space. For example, under the 32-bit JVM, in addition to the default storage structure of Mark Word listed above, there are the following structures that may change:
write picture description here

Among them, lightweight locks and biased locks are newly added after the optimization of synchronized locks in Java 6. We will briefly analyze them later. Here we mainly analyze the heavyweight lock, which is usually called the synchronized object lock. The lock identification bit is 10, and the pointer points to the starting address of the monitor object (also known as the monitor or monitor lock). Each object has a monitor associated with it, and the relationship between an object and its monitor can be implemented in various ways. For example, the monitor can be created and destroyed together with the object or automatically generated when a thread tries to acquire the object lock, but when a monitor is Once held by a thread, it is locked. In the Java virtual machine (HotSpot), the monitor is implemented by ObjectMonitor, and its main data structure is as follows (located in the ObjectMonitor.hpp file of the HotSpot virtual machine source code, implemented in C++)

ObjectMonitor() {
    _header       = NULL;
    _count        = 0; //记录个数
    _waiters      = 0,
    _recursions   = 0;
    _object       = NULL;
    _owner        = NULL;
    _WaitSet      = NULL; //处于wait状态的线程,会被加入到_WaitSet
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;
    FreeNext      = NULL ;
    _EntryList    = NULL ; //处于等待锁block状态的线程,会被加入到该列表
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
  }

There are two queues in ObjectMonitor, _WaitSet and _EntryList, which are used to save a list of ObjectWaiter objects (each thread waiting for a lock will be encapsulated into an ObjectWaiter object), _owner points to the thread holding the ObjectMonitor object, when multiple threads access a synchronization at the same time When the code is executed, it will first enter the _EntryList collection. When the thread obtains the monitor of the object, it enters the _Owner area and sets the owner variable in the monitor to the current thread and the counter count in the monitor is incremented by 1. If the thread calls the wait() method, The currently held monitor is released, the owner variable is restored to null, the count is decremented by 1, and the thread enters the WaitSet collection and waits to be woken up. If the current thread finishes executing, it will release the monitor (lock) and reset the value of the variable, so that other threads can enter to acquire the monitor (lock). As shown below
write picture description here

From this point of view, the monitor object exists in the object header of each Java object (pointing to the stored pointer), and the synchronized lock acquires the lock in this way, which is why any object in Java can be used as a lock, and at the same time It is also the reason why methods such as notify/notifyAll/wait exist in the top-level object Object.

So, how is method-level modification implemented?

The underlying principle of synchronized method:

Synchronization at the method level is implicit, that is, not controlled by bytecode instructions, and is implemented in method invocation and return operations. The JVM can distinguish whether a method is a synchronized method from the ACC_SYNCHRONIZED access flag in the method table structure (method_info Structure) in the method constant pool. When the method is called, the calling instruction will check whether the ACC_SYNCHRONIZED access flag of the method is set. If it is set, the executing thread will first hold the monitor (the word monitor is used in the virtual machine specification), then execute the method, and finally The monitor is released when the method completes (whether normally or abnormally). During the execution of the method, the execution thread holds the monitor, and no other thread can obtain the same monitor. If an exception is thrown during the execution of a synchronized method, and the exception cannot be handled inside the method, the monitor held by the synchronized method will be automatically released when the exception is thrown outside the synchronized method. Let's take a look at how the bytecode level is implemented:

public class SyncMethod {

   public int i;

   public synchronized void syncTask(){
           i++;
   }
}

The bytecode decompiled using javap is as follows:

Classfile /Users/zejian/Downloads/Java8_Action/src/main/java/com/zejian/concurrencys/SyncMethod.class
  Last modified 2017-6-2; size 308 bytes
  MD5 checksum f34075a8c059ea65e4cc2fa610e0cd94
  Compiled from "SyncMethod.java"
public class com.zejian.concurrencys.SyncMethod
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool;

   //省略没必要的字节码
  //==================syncTask方法======================
  public synchronized void syncTask();
    descriptor: ()V
    //方法标识ACC_PUBLIC代表public修饰,ACC_SYNCHRONIZED指明该方法为同步方法
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED
    Code:
      stack=3, locals=1, args_size=1
         0: aload_0
         1: dup
         2: getfield      #2                  // Field i:I
         5: iconst_1
         6: iadd
         7: putfield      #2                  // Field i:I
        10: return
      LineNumberTable:
        line 12: 0
        line 13: 10
}
SourceFile: "SyncMethod.java"

As can be seen from the bytecode, the synchronized modified method does not have the monitorenter instruction and monitorexit instruction. Instead, it is indeed the ACC_SYNCHRONIZED flag, which indicates that the method is a synchronized method. The JVM uses the ACC_SYNCHRONIZED access flag to identify a Whether the method is declared as a synchronous method so that the corresponding synchronous call is performed. This is the basic principle of synchronized locks implemented on synchronized code blocks and synchronized methods. At the same time, we must also note that in the early version of Java, synchronized is a heavyweight lock, which is inefficient, because the monitor lock (monitor) is implemented by relying on the Mutex Lock of the underlying operating system, and the operating system implements thread locks. When switching between users, it is necessary to switch from user state to core state. The transition between this state takes a relatively long time, and the time cost is relatively high, which is why the early synchronization is inefficient. Fortunately, after Java 6, Java officially optimized the synchronization from the JVM level, so the current efficiency of synchronized locks is also well optimized. After Java 6, in order to reduce the performance consumption caused by acquiring and releasing locks, Lightweight locks and biased locks are introduced. Next, we will briefly understand Java's official optimization of synchronized locks at the JVM level.
Java Virtual Machine's Optimization of Synchronized

There are four lock states in total, no lock state, biased lock, lightweight lock and heavyweight lock. With the competition of locks, locks can be upgraded from biased locks to lightweight locks, and then upgraded heavyweight locks, but the upgrade of locks is one-way, that is to say, it can only be upgraded from low to high, and there will be no locks. Downgrading, we have analyzed the heavyweight locks in detail before. Next, we will introduce biased locks, lightweight locks and other optimization methods of the JVM. We do not intend to go deep into the implementation and conversion process of each lock. Explain the core optimization idea of ​​each lock provided by the Java virtual machine. After all, the specific process is relatively cumbersome. If you need to know the detailed process, you can refer to "In-depth Understanding of Java Virtual Machine Principles".

Bias lock

Biased lock is a new lock added after Java 6. It is an optimization method for locking operations. After research, it is found that in most cases, locks not only do not have multi-thread competition, but are always obtained by the same thread multiple times. , so in order to reduce the cost of acquiring locks by the same thread (which involves some CAS operations, which are time-consuming), biased locks are introduced. The core idea of ​​the biased lock is that if a thread acquires the lock, the lock enters the biased mode, and the structure of the Mark Word also becomes the biased lock structure. When the thread requests the lock again, there is no need to do any synchronization operation, that is The process of acquiring a lock saves a lot of operations related to lock application, thus improving the performance of the program. Therefore, in the case of no lock competition, the biased lock has a good optimization effect. After all, it is very likely that the same thread applies for the same lock many times in a row. However, in the case of fierce lock competition, the biased lock will be invalid, because it is very likely that the threads applying for the lock are different each time. Therefore, the biased lock should not be used in this case, otherwise the gain will outweigh the loss. The thing is, after the biased lock fails, it will not be expanded to a heavyweight lock immediately, but will be upgraded to a lightweight lock first. Next, let's learn about lightweight locks.

Lightweight lock

If the biased lock fails, the virtual machine will not be upgraded to a heavyweight lock immediately, it will also try to use an optimization method called a lightweight lock (added after 1.6), and the structure of Mark Word also becomes light. The structure of the magnitude lock. The basis that lightweight locks can improve program performance is that "for most locks, there is no competition during the entire synchronization cycle", note that this is empirical data. What needs to be understood is that the light-weight lock is suitable for the situation where threads alternately execute synchronized blocks. If there is a situation where the same lock is accessed at the same time, the light-weight lock will expand into a heavy-weight lock.

spin lock

After the lightweight lock fails, the virtual machine will perform an optimization method called spin lock in order to prevent the thread from actually suspending at the operating system level. This is based on the fact that in most cases, the thread holding the lock will not be too long. If the thread at the operating system level is directly suspended, it may not be worth the gain. After all, the operating system needs to switch from user mode to user mode when switching between threads. The core state, the transition between this state takes a relatively long time and the time cost is relatively high, so the spin lock will assume that in the near future, the current thread can obtain the lock, so the virtual machine will let the current thread that wants to acquire the lock. Do a few empty loops (this is also called spin), generally not too long, maybe 50 loops or 100 loops, after several cycles, if the lock is obtained, it will enter the critical section smoothly. If the lock cannot be obtained, the thread will be suspended at the operating system level. This is the optimization method of spin locks, and this method can indeed improve efficiency. In the end, there was no other way but to upgrade to a heavyweight lock.

lock removal

Eliminating locks is another kind of lock optimization for virtual machines. This optimization is more thorough. Java virtual machines are compiled when JIT is compiled (which can be simply understood as compiling when a certain piece of code is about to be executed for the first time, also known as just-in-time compilation), By scanning the running context, the locks that are impossible to compete for shared resources are removed, and unnecessary locks are eliminated in this way, which can save the time of meaningless lock requests. The append method of StringBuffer is a synchronization method, but in the add method The StringBuffer in is a local variable and will not be used by other threads, so it is impossible for StringBuffer to have a shared resource competition situation, and the JVM will automatically remove its lock.

Waiting for the wake-up mechanism and synchronized

The so-called waiting wake-up mechanism mainly refers to the notify/notifyAll and wait methods. When using these three methods, you must be in the synchronized code block or synchronized method, otherwise an IllegalMonitorStateException exception will be thrown, because these three methods are called. The monitor object of the current object must be obtained before the method, that is to say, the notify/notifyAll and wait methods depend on the monitor object. In the previous analysis, we know that the monitor exists in the Mark Word of the object header (stores the monitor reference pointer) , and the synchronized keyword can get the monitor, which is why the notify/notifyAll and wait methods must be called in the synchronized code block or synchronized method.

synchronized (obj) {
       obj.wait();
       obj.notify();
       obj.notifyAll();         
 }

One thing to understand is that, unlike the sleep method, the thread will be suspended after the wait method is called, but the wait method will release the monitor lock (monitor) currently held until a thread calls the notify/notifyAll method. Can continue to execute, and the sleep method only puts the thread to sleep and does not release the lock. At the same time, after the notify/notifyAll method is called, the monitor lock will not be released immediately, but the lock will be automatically released after the corresponding synchronized(){}/synchronized method is executed.

Reprinted: https://blog.csdn.net/javazejian/article/details/72828483
Always introspect, keep learning, find your own shortcomings, and try to make yourself go further, and encourage each other!

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=325950485&siteId=291194637