In-depth understanding of the synchronized keyword

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

synchronized
It is a very important keyword for the Java concurrency module . It is a built-in synchronization mechanism in Java and represents a certain concept of internal locking.
Share resource
After locking, other threads that want to obtain shared resources must 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. 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
  • Synchronized modification of the 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 of a 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 () { //... } Copy code

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); } } Copy code

The above output result 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, indicating 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. The run method contains 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 the synchronized void Increase () {} copy the code

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++; } } } Copy code

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 there are other threads 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

//Instance object lock synchronized ( this ){ for ( int j = 0 ;j < 1000 ;j++){ i++; } } //class object lock synchronized (TSynchronized.class){ for ( int j = 0 ;j < 1000 ;j++){ i++; } } Copy code

The underlying principle of synchronized

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

We may all know (we will analyze in detail below) that the synchronized code block is implemented by a set of monitorenter/monitorexit instructions. and

Monitor
The object is the basic unit to achieve synchronization.

What is

Monitor
What about the object?

Monitor object

Any object is associated with a monitor, and the monitor is a mechanism for controlling concurrent access to objects .

Tube
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

Mutually exclusive
. Mutual exclusion guarantees 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

hotspot
In the virtual machine, the layout of objects 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 content of the above object in detail.

Header

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.

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.

Translated in Chinese is

  • Stateless is
    no lock
    At the time, the object header opens up 25 bits of space to store the object's hashcode, 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 as 01.
  • Bias lock
    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.
  • Lightweight lock
    Directly open up 30 bit space to store the pointer to the lock record in the stack, and 2 bit store the lock flag, and its flag bit is 00.
  • Heavyweight lock
    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 mark
    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.

Regarding why the memory is so allocated, we can read from

OpenJDK
The enumeration in the markOop.hpp class shows the clues

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 the HotSpot virtual machine, the underlying code of the monitor is composed of

ObjectMonitor
The main data structure is as follows (located in the ObjectMonitor.hpp file of the HotSpot virtual machine source code, implemented in C++)

There are several attributes that need to be paid attention to in this C++: _WaitSet, _EntryList and _Owner, each thread waiting to acquire the lock will be encapsulated as

ObjectWaiter
Object.

_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 feel this process, and then we will separate it

no lock

No lock state
, 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.

The feature of lock-free is that the modification operation is performed 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, there is not only no multi-threaded competition for locks, but also a situation where the lock is acquired multiple times by the same thread. In this case, the biased lock appears. It appears to solve the problem. Improve performance when a thread performs synchronization.

It can be seen from the allocation of the object header that the biased lock is much more than the lock-free

Thread ID
with
epoch
, Let s describe the acquisition process of the bias lock

Partial lock acquisition process

  1. 1. the thread accesses the synchronization code block, and it will check the Mark Word of the object header
    Lock flag
    Determine the current lock status. If it is 01, it means that it is unlocked or biased, and then based on
    Whether it is biased towards lock
    To judge whether it is lock-free or bias-locked. If it is lock-free, 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, and 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 bottom of the C language, here is a simple understanding that SafePoint is the location where a thread in the Java code may suspend execution.

There is no need to wait until the next time the thread enters and exits the synchronized code block

CAS
To perform locking and unlocking operations, you only need to simply judge whether the thread ID pointing to the current thread is stored in the Mark Word of the object header. Of course, the judgment flag is judged according to the lock flag bit. If it is represented by a flowchart, it is as follows

Close the bias lock

Bias lock is the default in Java 6 and Java 7

Enable
of. Since the bias lock is to improve performance when only one thread executes 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=false
, Then the program will enter the lightweight lock state by default.

About epoch

One of the object headers that are biased to lock is called

epoch
It serves as a timestamp of the validity of the deviation.

Lightweight lock

Lightweight lock
Refers to when the current lock is a biased lock, and the resource is accessed by another thread, then the biased lock will be upgraded to
Lightweight lock
, Other threads will pass
Spin
In the form of trying to acquire the lock, it will not block, 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 CAS operation is used to replace the ThreadID and it fails (switch to another thread's perspective), 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. Thread arrives
    Global Safe Point (SafePoint)
    At the time, the thread that originally held the biased lock will be suspended, and then the status of the original held biased lock will be checked. 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)

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, the next step is executed, 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

  • 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 will be released, and the waiting thread will be 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
    Heavyweight lock

If you use the flowchart to show it is like this

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++; } } } Copy code

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++; } } Copy code

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 have six PDFs on my own, and they have spread more than 10w+ on the Internet. After searching for "programmer cxuan" on WeChat and following the official account, I reply to cxuan in the background and receive all the PDFs. These PDFs are as follows

6.PDF links