In-depth understanding of the synchronized keyword

Preface

The importance of the synchronized keyword is self-evident, it can almost be said that it is a keyword that must be asked for concurrent and multithreaded. Synchronized will involve locks, upgrade and downgrade operations, lock revocation, object headers, etc. Therefore, it is very important to understand synchronization. This article will take you from the basic usage of synchronized to the in-depth understanding of synchronized and the first class of objects, and uncover the veil of synchronization for you.

Analysis of synchronized

synchronizedIt is a very important keyword for the Java concurrency module . It is a built-in synchronization mechanism in Java, which represents a certain internal locking concept. When a thread 共享资源locks a certain thread , other threads that want to obtain shared resources must perform Wait, synchronized also has mutually exclusive and exclusive semantics.

What is mutual exclusion? We must have played with magnets when we were young. Magnets have the concept of positive and negative poles. The same sex repels the opposite sex and attracts each other. Repulsion is equivalent to a concept of mutual exclusion, that is, the two are incompatible with each other.

Synchronized is also an exclusive keyword, but its exclusive semantics is more to increase thread safety, by monopolizing a certain resource to achieve mutual exclusion and exclusive purpose.

After understanding the semantics of exclusiveness and mutual exclusion, let's take a look at the usage of synchronized first, first to understand the usage, and then to understand the underlying implementation.

The use of synchronized

You probably know everything about synchronized

  • Synchronized modification of the instance method is equivalent to locking the instance of the class, and you need to obtain the lock of the current instance before entering the synchronization code
  • Synchronized modification of static methods is equivalent to locking the class object
  • The synchronized modification code block is equivalent to locking the object. You need to obtain the lock of the object before entering the code block

Below we explain each usage

synchronized modified instance method

Synchronized modified instance method, an instance method is an instance belonging to the class. The instance method modified by synchronized is equivalent to an object lock. The following is an example of synchronized modification instance method.

public synchronized void method()
{
   // ...
}

Like the above synchronized modification method is an instance method, let’s get to know the synchronized modification instance method through a complete example

public class TSynchronized implements Runnable{

    static int i = 0;

    public synchronized void increase(){
        i++;
        System.out.println(Thread.currentThread().getName());
    }


    @Override
    public void run() {
        for(int i = 0;i < 1000;i++) {
            increase();
        }
    }

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

        TSynchronized tSynchronized = new TSynchronized();
        Thread aThread = new Thread(tSynchronized);
        Thread bThread = new Thread(tSynchronized);
        aThread.start();
        bThread.start();
        aThread.join();
        bThread.join();
        System.out.println("i = " + i);
    }
}

The result of the above output is i = 2000, and the current ready-made name will be printed every time

To explain the above code, the i in the code is a static variable, and the static variable is also a global variable. The static variable is stored in the method area. The increase method is decorated by the synchronized keyword, but it is not decorated with the static keyword, which means that the increase method is an instance method. Every time a TSynchronized class is created, an increase method is created. In the increase method, only the currently accessed thread name is printed. . The Synchronized class implements the Runnable interface and rewrites the run method. In the run method is a counter from 0 to 1000. There is nothing to say about this. In the main method, new has two threads, aThread and bThread. Thread.join means waiting for the end of this thread. The main function of this code is to determine whether the synchronized modification method can be exclusive.

synchronized modified static method

Synchronized modification of static methods is the use of synchronized and static keywords together

public static synchronized void increase(){}

When synchronized acts on a static method, it means the lock of the current class. Because the static method belongs to the class, it does not belong to any instance member, so concurrent access can be controlled through the class object.

One thing to note here, because the synchronized modified instance method belongs to the instance object, and the synchronized modified static method belongs to the class object, so calling the synchronized instance method will not prevent access to the synchronized static method.

synchronized modified code block

In addition to modifying instance methods and static methods, synchronized can also be used to modify code blocks, which can be nested inside the method body.

public void run() {
  synchronized(obj){
    for(int j = 0;j < 1000;j++){
      i++;
    }
  }
}

In the above code, obj is used as a lock object to lock it. Every time a thread enters the synchronized modified code block, the current thread will be required to hold the obj instance object lock. If other threads are currently holding the object lock, then the new The arriving thread must wait.

The synchronized modified code block can not only lock the object, but also lock the current instance object lock and class object lock

// 实例对象锁
synchronized(this){
    for(int j = 0;j < 1000;j++){
        i++;
    }
}

//class对象锁
synchronized(TSynchronized.class){
    for(int j = 0;j < 1000;j++){
        i++;
    }
}

The underlying principle of synchronized

After a brief introduction to synchronized, let's talk about the underlying principle of synchronized.

We may all know (we will analyze in detail below) that the synchronized code block is implemented by a set of monitorenter/monitorexit instructions. The Monitorobject is the basic unit to achieve synchronization.

What is the Monitorobject?

Monitor object

Any object is associated with a monitor, and the monitor is a mechanism for controlling concurrent access to objects . 管程It is a synchronization primitive, which refers to synchronized in Java, which can be understood as the implementation of monitor in Java.

The monitor provides an exclusive access mechanism, which is also the mechanism 互斥. Mutual exclusion ensures that at each point in time, at most one thread will execute the synchronization method.

So you understand that the Monitor object is actually an object that uses monitors to control synchronous access.

Object memory layout

In the hotspotvirtual machine, the object layout in memory is divided into three areas:

  • 对象头(Header)
  • 实例数据(Instance Data)
  • 对齐填充(Padding)

The memory distribution of these three areas is shown in the figure below

Let's introduce the contents of the above object in detail.

The object header Header mainly contains MarkWord and the object pointer Klass Pointer. If it is an array, it also contains the length of the array.

img

In a 32-bit virtual machine, MarkWord, Klass Pointer and array length occupy 32 bits, which is 4 bytes.

If it is a 64-bit virtual machine, MarkWord, Klass Pointer and array length occupy 64 bits, which is 8 bytes.

The byte size of Mark Word in 32-bit virtual machine and 64-bit virtual machine is different. Mark Word and Klass Pointer in 32-bit virtual machine occupy 32-bit bytes respectively, while Mark Word and Klass in 64-bit virtual machine Pointer occupies 64 bits of bytes. Let's take a 32-bit virtual machine as an example to see how the Mark Word bytes are allocated.

img
img

Translated in Chinese is

img
  • Stateless, that is 无锁, when the object header opens up 25 bits of space to store the hashcode of the object, 4 bits are used to store the generational age, 1 bit is used to store the identification bit of whether the lock is biased, and 2 bits are used to store the lock identification bit Is 01.
  • 偏向锁 The division in the medium is more detailed, or open up 25 bits of space, of which 23 bits are used to store thread IDs, 2 bits are used to store epochs, 4 bits are used to store generational ages, and 1 bit stores whether they are biased to lock identification, 0 means no lock, 1 means biased lock , The identification bit of the lock is still 01.
  • 轻量级锁Directly open up a 30-bit space to store the pointer to the lock record in the stack, 2bit stores the lock flag, and its flag bit is 00.
  • 重量级锁Medium and lightweight locks are the same. 30 bits of space are used to store pointers to heavyweight locks, and 2 bits are used to store lock identification bits, which are 11
  • GC标记The 30-bit memory space is not occupied, and the 2 bit space to store the lock flag is 11.

Among them, the lock flag bits of both lock-free and biased locks are both 01, but the first 1 bit distinguishes whether this is a lock-free state or a biased lock state.

Why so on memory allocation, we can from OpenJDKthe markOop.hpp gleaned clues class enumeration

img

To explain

  • age_bits is what we call the identification of generational collection, which occupies 4 bytes
  • lock_bits is the lock flag, occupying 2 bytes
  • biased_lock_bits is the identification of biased lock, occupying 1 byte.
  • max_hash_bits is the number of bytes occupied by hashcode calculated for lock-free, if it is a 32-bit virtual machine, it is 32-4-2 -1 = 25 byte, if it is a 64-bit virtual machine, 64-4-2-1 = 57 byte, But there will be 25 bytes unused, so the 64-bit hashcode occupies 31 bytes.
  • hash_bits is for a 64-bit virtual machine, if the maximum number of bytes is greater than 31, then take 31, otherwise take the real number of bytes
  • cms_bits I think it should be a 64-bit virtual machine that occupies 0 byte, and a 64-bit virtual machine occupies 1 byte
  • epoch_bits is the byte size occupied by epoch, 2 bytes.

In the above virtual machine object header allocation table, we can see that there are several lock states: lock-free (stateless), biased lock, lightweight lock, heavyweight lock, of which lightweight lock and biased lock are In JDK 1.6, the synchronized lock was newly added after optimization. Its purpose is to greatly optimize the performance of the lock. Therefore, in JDK 1.6, the overhead of using synchronized is not that big. In fact, in terms of whether the lock is locked or not, there are still only lock-free and heavyweight locks. The emergence of biased locks and lightweight locks has increased the lock acquisition performance, and no new locks have appeared.

So our focus is on the study of synchronized heavyweight locks. When the monitor is held by a thread, it will be in a locked state. In HotSpot virtual machine, Monitor underlying code is ObjectMonitorimplemented, the main data structure is as follows (in the HotSpot VM ObjectMonitor.hpp source files, C ++ implementation)

This C ++ need to pay attention to several attributes: _WaitSet, _EntryList and _Owner, each thread is waiting to acquire the lock will be called encapsulated ObjectWaiterobject.

_Owner refers to the thread that points to the ObjectMonitor object, and _WaitSet and _EntryList are used to save the list of each thread.

So what is the difference between these two lists? I'll talk about the lock acquisition process with you about this issue, and you will be clear.

Two lists of locks

When multiple threads access a certain piece of synchronization code at the same time, they will first enter the _EntryList collection. When the thread obtains the monitor of the object, it will enter the _Owner area, and point the _Owner of the ObjectMonitor object to the current thread, and make _ count + 1, if the lock release operation (such as wait) is called, the currently held monitor will be released, owner = null, _count-1, and the thread will enter the _WaitSet list and wait to be awakened. If the current thread finishes executing, the monitor lock will also be released, but the _WaitSet list will not be entered at this time, but the value of _count will be reset directly.

Klass Pointer represents a type pointer, that is, an object pointer to its class metadata. The virtual machine uses this pointer to determine which class instance the object is.

You may not fully understand what a pointer is. You can simply understand that a pointer is an address that points to a certain data.

Instance Data

The instance data part is the effective information actually stored by the object, and it is also the byte size of each field defined in the code. For example, a byte occupies 1 byte, and an int occupies 4 bytes.

Align Padding

Alignment is not necessary, it only functions as a **placeholder (%d, %c, etc.)**. This is the requirement of the JVM, because HotSpot JVM requires that the starting address of the object must be an integer multiple of 8 bytes, which means that the byte size of the object is an integer multiple of 8. If it is not enough, you need to use Padding to complete.

Lock upgrade process

Let’s first come to a general flow chart to experience this process, and then let’s talk about it separately.

img

no lock

无锁状态, Lock-free means that the resource is not locked, all threads can access the same resource, but only one thread can successfully modify the resource.

img

The feature of lock-free is to modify the operation in the loop. The thread will continue to try to modify the shared resource until it can successfully modify the resource and exit. There is no conflict in the process, which is very similar to the CAS introduced in the previous article. Realization, the principle and application of CAS is the realization of lock-free. Lock-free cannot fully replace with lock, but the performance of lock-free in some situations is very high.

Bias lock

The author of HotSpot found through research that in most cases, not only does the lock have no multi-thread competition, but there is also a situation where the lock is acquired multiple times by the same thread. The biased lock appears in this case, and it appears to solve the problem. Improve performance when a thread performs synchronization.

img

Objects can be seen from the distribution head, the biased locking lock-free more than 线程IDand epoch, here we come to describe the biased locking acquisition process

Partial lock acquisition process

  1. First, the thread accesses the synchronization code block, and 锁标志位judges the current lock status by checking the object header Mark Word . If it is 01, it means it is lock-free or bias-locked, and then 是否偏向锁judges whether it is lock-free or bias-locked according to the label, if it is lock-free In case, proceed to the next step
  2. The thread uses the CAS operation to try to lock the object. If the ThreadID is successfully replaced with CAS, it means that the lock is the first time. Then the current thread will obtain the bias lock of the object. At this time, the current will be recorded in the Mark Word of the object header. Thread ID and lock time epoch and other information, and then execute the synchronization code block.
Global safe point (Safe Point): The understanding of the global safe point will involve some knowledge of the underlying C language, here is a simple understanding that SafePoint is the location where a thread in the Java code may suspend execution.

When a thread enters and exits the block synchronization code does not need to wait for the CASoperation for locking and unlocking, simply click Mark Word determines whether the object is stored in the header points to the current thread's thread ID, of course, according to the judgment flag It is judged by the lock flag. If it is represented by a flowchart, it is as follows

img

Close the bias lock

The bias lock is the default in Java 6 and Java 7 启用. Since the bias lock is to improve performance when there is only one thread executing the synchronized block, if you are sure that all the locks in the application are in a competitive state under normal circumstances, you can turn off the bias lock through the JVM parameter:, -XX:-UseBiasedLocking=falsethen the program will enter the lightweight lock by default status.

About epoch

Biased locking head object is called a epochvalue, as a time stamp deviation effectiveness.

Lightweight lock

轻量级锁It means that when the current lock is a biased lock, the resource is accessed by another thread, then the biased lock will be upgraded to a form 轻量级锁that other threads will 自旋try to acquire the lock without blocking, thereby improving performance. The following is the detailed acquisition process .

Lightweight lock lock process

  1. Immediately after the previous step, if the CAS operation to replace the ThreadID is not obtained successfully, proceed to the next step
  2. If the use of CAS operation to replace ThreadID fails (this time switch to the perspective of another thread), it means that the resource has been accessed synchronously. At this time, the lock cancellation operation will be executed, the biased lock will be cancelled, and then the original holder of the biased lock will be waited. When the thread arrives 全局安全点(SafePoint), it will suspend the thread that originally held the biased lock, and then check the status of the original biased lock. If the synchronization has been exited, the thread holding the biased lock will be awakened, and the next step will be executed.
  3. Check whether the Mark Word in the object header records the current thread ID, if it is, execute the synchronization code, if not, execute the second step of the bias lock acquisition process .

If it is expressed by the process, it is as follows (it already includes the acquisition of the biased lock)

img

Heavyweight lock

The heavyweight lock is actually the process of synchronized and finally locking. Before JDK 1.6, it was the process of unlocking -> locking.

The process of obtaining heavyweight locks

  1. Then the acquisition process of the above bias lock is upgraded from the bias lock to a lightweight lock, and the next step is to
  2. The lock record will be allocated in the stack of the thread that originally held the biased lock, the Mark Word in the object header will be copied to the record of the thread that originally held the biased lock, and then the thread that originally held the biased lock will obtain a lightweight lock, and then wake up The thread that originally held the biased lock continues to execute from the safe point. After the execution is completed, execute the next step, and the current thread executes step 4.
  3. After the execution is complete, start the lightweight unlocking operation, unlocking requires two conditions to be judged
  • Determine whether the lock record pointer in the Mark Word in the object header points to the pointer of the record in the current stack
img
  • Whether the Mark Word information copied in the current thread lock record is consistent with the Mark Word in the object header.

If the above two judgment conditions are met, the lock is released. If one of the conditions is not met, the lock is released, and the waiting thread is aroused for a new round of lock competition.

  1. Allocate the lock record in the stack of the current thread, copy the MarkWord in the object header to the lock record of the current thread, and execute the CAS lock operation. The lock record pointer in the Mark Word object header will point to the current thread lock record. If successful, get Lightweight lock, execute synchronization code, and then execute step 3. If unsuccessful, execute the next step
  2. The current thread does not use CAS to successfully acquire the lock, it will spin for a while and try to acquire again. If the lock is not acquired after multiple spins reach the upper limit, then the lightweight lock will be upgraded to 重量级锁
img

If you use the flowchart to show it is like this

img

Based on the detailed description of lock upgrades above, we can summarize the applicable scope and scenarios of different locks.

Low-level implementation of synchronized code block

In order to facilitate research, we simplify the example of synchronized modified code block, as shown in the following code

public class SynchronizedTest {

    private int i;

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

}

We mainly focus on the synchronized bytecode, as shown below

From this bytecode, we can know that the synchronization statement block uses the monitorenter and monitorexit instructions, where the monitorenter instruction points to the beginning of the synchronization code block, and the monitorexit instruction points to the end of the synchronization code block.

So why are there two monitorexit?

I wonder if you noticed the exception table below? If you don’t know what an exception table is, then I suggest you read this article

After reading this Exception and Error, it’s okay to wrestle with the interviewer

The underlying principle of synchronized modification method

The synchronization of the method is implicit, which means that the bottom layer of the synchronized modification method does not need to be controlled by bytecode. Is this really the case? Let's decompile a wave to see the results

public class SynchronizedTest {

    private int i;

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

This time we use javap -verbose to output detailed results

It can be seen from the bytecode that the synchronized modification method does not use the monitorenter and monitorexit instructions. Instead, the ACC_SYNCHRONIZED flag is obtained. The flag indicates that this method is a synchronized method. The JVM uses the ACC_SYNCHRONIZED access flag to identify whether a method is Declare it as a synchronous method to execute the corresponding synchronous call. This is the difference between the synchronized lock on the synchronized code block and the synchronized method.

I had six PDFs on my own, and they spread more than 10w+ on the Internet. After searching "programmer cxuan" on WeChat and following the official account, I responded to cxuan in the background and received all the PDFs. These PDFs are as follows

Six PDF links