当前位置:网站首页>Depth analysis based on synchronized lock

Depth analysis based on synchronized lock

2020-11-09 10:49:00 AnonyStar



1. Problem introduction


The kids have been exposed to threads , They also use threads , Today we are going to talk about thread safety , Before that, let's look at a simple code case .


Code case :

/**
 * @url: i-code.online
 * @author: AnonyStar
 * @time: 2020/10/14 15:39
 */
public class ThreadSafaty {
    // Shared variables 
    static int count = 0;

    public static void main(String[] args) {

        // Create thread 
        Runnable runnable = () -> {
            for (int i = 0; i < 5; i++) {
                count ++;
                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };

        for (int i = 0; i < 100; i++) {
            new Thread(runnable,"Thread-"+i).start();
        }

        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("count = "+ count);
    }
}

Execution results :



Problem specification :
In the above code we can see , Defines a thread runnable The public member variables are ++ operation , And cycle five times , One millisecond at a time , And then we're in the main thread main Method to create 100 threads and start , Then the main thread sleeps for five seconds to wait for all threads to finish . We expect the result to be 500 . But after the actual implementation, we found that count The value of is not fixed , Less than 500 Of , This is the problem of data security caused by multithreading !

Through the above cases, we can clearly see the problem of thread safety , So let's see if there is any way to avoid this kind of security problem ? We can imagine that the reason for this security problem is that we have access to shared data , So whether we can change the process of thread access to shared data into a serial process, then there is no such problem . Here we can think of what we said before lock , We know that locking is a synchronous way to handle concurrency , At the same time, it is also mutually exclusive , stay Java Locking is achieved by synchronized keyword


2. The basic knowledge of lock

2.1 Synchronized The understanding of

stay Java We know that there is a keyword of the rank of the elder synchronized , It's the key to locking , But we always think of it as a heavyweight lock , In fact as early as jdk1.6 A lot of optimization has been done on it , Make it very flexible . It's not always a heavyweight lock anymore , It's the introduction of ** Biased locking ** and ** Lightweight lock . ** We will introduce in detail .

synchronized The basic use of

  • synchronized modification Example method , Locks on the current instance
  • synchronized modification Static methods , Locks on the current class object ,
  • synchronized modification Code block , Specify the lock object , Lock a given object ,

In the above case , We're going to get into being synchronized Modify the synchronization code before , The corresponding lock must be obtained , In fact, this is also reflected in different types of modification , Represents the control granularity of the lock

  • Let's revise the case we wrote earlier , By using synchronized Keywords make it thread safe
 // Create thread 
        Runnable runnable = () -> {
            synchronized (ThreadSafaty.class){
                for (int i = 0; i < 5; i++) {
                    count ++;
                    try {
                        Thread.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }

        };

Just add synchronized (ThreadSafaty.class) Modification of , Put the content of the operation into the code block , Then thread safety will be achieved

  • Through the above practice, we can intuitively feel synchronized The role of , This is our usual use in development , Have you ever wondered , How is this lock stored and implemented ? So we're going to explore the mystery of it

Java The realization of middle lock

  • We know that locks are mutually exclusive (Mutual Exclusion) Of , So where does it mark its existence ?
  • We also know that multiple threads can acquire locks , Then locks must be shared
  • What we are most familiar with synchronized What's the process of getting the lock ? How is its lock stored ?
  • We can observe synchronized The grammar of , You can see synchronized(lock) Is based on lock To control the lock granularity , It must be understood here that , When we get the lock, we are an object , So is the lock related to this object ?
  • So far , We point all the key information to the object , So we need to take this as a starting point , Let's first understand that the object is in jvm The distribution form in , Let's look at how locks are implemented .

Memory layout of objects

  • Here we only talk about objects in Heap Layout in , It doesn't involve too many details about the object creation process , We will elaborate on these contents in a separate article , Can pay attention to i-code.online Blog or wx " Yunqi code "
  • In our most commonly used virtual machine hotspot The distribution of objects in memory can be divided into three parts : Object head (Header)、 Real data (Instance Data)、 The filling (Padding

  • From the above diagram, we can see that , Object in memory , It consists of three parts ,    The head of the object is divided into Object tags and class meta information , In the object tag, it mainly includes as shown in the figure hashcode、GC Generational age 、 Lock flag State 、 Biased lock holding threads id、 A lock held by a thread (monitor) Wait for six things , The length of this part of data is 32 Bit and 64 The virtual machines of bit are respectively 32bit and 64bit, Officially, this part is called Mark Word .
  • Mark Word It's actually a data structure that can be dynamically defined , This allows a very small space to store as much data as possible , Reuse your own memory space according to the state of the object , For example 32 Bit of virtual machine , If the object is not locked by a synchronization lock , Mark Word Of 32 In a bit memory cell ,25 To store hashcode ,4 One for storage GC Generational age ,2 Latch flag bits ,1 A fixed position 0, According to the distribution of each state, you can directly refer to the following chart


32 position HotSpot Virtual machine object header Mark Word

The lock state 25bit 4bit 1bit
( Is it biased lock )
2bit
( Lock flag position )
23bit 2bit
unlocked Object's HashCode Generational age 0 01
Biased locking Threads ID Epoch( Bias timestamp ) Generational age 1 01
Lightweight lock Pointer to the lock record in the stack 00
Heavyweight lock A pointer to a heavyweight lock 10
GC Mark empty 11

What is said above is 32 Bit virtual machine , We need to pay attention to . Another part of the object header is the type pointer , Let's not go into details here , The concerns you want to know i-code.online , Will continue to update the relevant content

  • The following content will refer to the source code view , Need to download the source code in advance , If you don't know how to download , You can see 《 download JDK And Hotspot Virtual machine source code 》 This article , Or attention Yunqi code .
  • In our familiar virtual machine Hotspot To realize Mark Word Code in markOop.cpp in , We can see the following clip , This is a description of the virtual machine MarkWord Storage layout of :

  • When we're in new When an object , The virtual machine layer actually creates a instanceOopDesc object , Familiar to us Hotspot Virtual machine uses OOP-Klass Model to describe Java Object instances , among OOP It's the familiar common object pointer , and Klass It describes the specific types of objects , stay Hotspot We use instanceOopDesc and arrayOopDesc To describe , among arrayOopDesc Used to describe array types ,
  • about instanceOopDesc We can realize it from Hotspot Source code found in . Corresponding to instanceOop.hpp In file , And the corresponding arrayOopDesc stay arrayOop.hpp in , Let's take a look at the relevant content :

  • We can see instanceOopDesc Inherited oopDesc, and oopDesc It is defined in oop.hpp in ,

  • We can see the relevant information in the above figure , The text is also annotated , So next we're going to explore _mark The implementation of defines , as follows , We see it is markOopDesc

  • Through code follow-up, we can find markOopDesc In the definition of markOop.hpp In file , As shown in the figure below :
  • In the above picture, we can see , There's an enumeration inside . Recorded markOop Storage items in , So when we actually develop , When synchronized When an object is used as a lock, then the information of the following series of locks is the same as markOop relevant . As in the table above mark word The distribution record shows the meaning of the specific parts
  • Because we're actually creating objects in jvm Layers will generate a native Of c++ object oop/oopdesc To map , And every object has a monitor Monitor object for , Can be in markOop.hpp see , In fact, in multithreading, seizing the lock is just fighting for monitor To modify the corresponding tag

Synchronized In depth

  • stay Java in synchronized Is the most basic method to achieve mutual exclusion synchronization , It's a block structure (Block Structured) Synchronization syntax of , after javac After compiling, it will be formed before and after the block monitorrenter and monitorexit Two bytecode instructions , And they all need one reference Type to indicate the lock object , The specific lock object depends on synchronized Decorated content , It has been said that it is not elaborated .

《 In depth understanding of Java virtual machine 》 This is described in :
according to 《Java Virtual machine specification 》 The requirements of , In execution monitorenter When the command , First try to get the lock of the object . If This object is not locked , Or the current thread already has a lock on that object , Just increase the lock counter by one , And in the execution monitorexit The value of the lock counter is subtracted by one during the instruction . Once the value of the counter is zero , The lock was then released . If you get an object Lock failed , Then the current thread should be blocked and waiting , Until the object requesting the lock is released by the thread holding it

  • So be synchronized Decorated blocks of code are reentrant to the same thread , This also avoids the possibility of deadlock caused by repeated entry of the same thread
  • stay synchronized Lock the end of the code directly before releasing , It will block other threads after

Why do you say synchronized It's a heavyweight lock

  • In terms of execution costs , Holding a lock is a heavyweight (Heavy-Weight) Operation process , Because in Java Threads in the operating system are mapped to the native kernel threads of the operating system , If you want to block and wake up a thread, you need to schedule it through the operating system , This will inevitably lead to the conversion between user mode and kernel mode , But this conversion is very processor time consuming , Especially for the program with simple business code , It may take longer than the execution of the business code itself , So synchronized It's a massive operation , But in the jdk6 After that, a lot of optimization has been done , Make it less heavy

Lock optimization

  • stay JDK5 Upgrade to JDK6 After a series of lock improvements , Optimize locks through a variety of techniques , Give Way synchronized No longer as heavy as before , This involves adaptive spin (Adaptive Spinning)、 Lock elimination (Lock Elimination)、 Lock expansion (Lock Coarsening)、 Lightweight lock (LightWeight Locking)、 Biased locking (Biased Locking) etc. , These are all used to optimize and improve the competition of multi-threaded access to shared data .

Lock elimination

  • Lock elimination is that the virtual machine requires synchronization of some code when the compiler is running , However, it is detected that there is no lock for shared data competition , The main criterion is based on escape analysis technology , I don't want to expand on this , The following related articles are introduced . Here we simply understand it as , If a piece of code , Data on the heap will not escape and be accessed by other threads , Then you can treat them as data on the stack , Think they're all thread private , Thus, there is no need to synchronize locking ,
  • About whether variables in the code escape , For virtual machines, complex analysis is needed to get , But it's relatively intuitive for us developers , Some people may wonder why they need extra lock synchronization since developers can understand it ?, Actually , A lot of synchronization measures on the program are not what we developers join in , It is java There's a lot of stuff inside , For example, the following typical example , Here is the addition of strings
    private String concatString(String s1,String s2,String s3){
        return s1 + s2 + s3;
    }
  • We know String Class is final Decorated immutable class , So the addition of strings is done by generating new String Let's try the first one , So the compiler optimizes this operation , stay JDK5 It will be converted to StringBuffer Object's append() operation , And in the JDK5 And then it turns into StringBuilder Object to operate . So the above code is in jdk5 It could become something like this :
    private String concatString(String s1,String s2,String s3){
        StringBuffer sb = new StringBuffer();
        sb.append(s1);
        sb.append(s2);
        sb.append(s3);
        return sb.toString();
    }
  • Now , You can see that , about StringBuffer.append() Method is a synchronous method , With synchronization fast , The lock object is sb , At this time, the virtual machine through the analysis found that sb The scope of is restricted to methods , It is impossible to escape from the method and let other threads access it , So this is after instant compilation by the server-side compiler , All synchronization actions of this code will fail and will be executed directly .

The above code is chosen for the convenience of demonstration String, Actually, it's in jdk5 And then it's all converted to Stringbuilder , There is no such problem , But in jdk There are a lot of them .

Lock coarsening

  • The vulgar language about lock is actually very simple to understand , We always recommend that synchronized code blocks be as small as possible during development , Try to synchronize only in the actual scope of the shared data , The goal is to minimize synchronization operations , Let other threads get the lock faster
  • This is most of the time , But there are always special circumstances , For example, in a series of continuous operations are to lock and unlock the same object repeatedly , Then this can lead to unnecessary performance loss
  • It's like the above String The case of , In succession append Operations are fragmented synchronization blocks , And it's all the same lock object , This will extend the scope of the lock , To the outside of the entire sequence of operations , That's the first one append From before to last append After the operation , Put them all in a synchronous lock , This avoids multiple lock acquisition and release .

spinlocks

  • Through previous understanding , We know that suspending thread and recovering thread involve the conversion between user mode and kernel state , And these are very time-consuming , This will directly affect the concurrent performance of the virtual machine .
  • In our normal development , If shared data is locked for a short period of time , It is a waste of resources to suspend the blocking thread for this short time . Especially now computers are basically multi-core processors , So on this premise , Whether we can let another thread that requests the lock object not to suspend , But wait a little bit , This wait will not give up CPU Execution time of . Wait to see if the thread holding the lock can release the lock quickly , In fact, this waiting is like an empty cycle , This technology is called spin
  • Spin locked in JDK6 Medium and is already enabled by default , stay jdk4 The introduction of . Spin lock is not blocking, it can't replace blocking .
  • Spinlocks require a certain number of processors , At the same time, it will occupy CPU The time of the , Although it avoids the overhead of thread switching , But there is a balance between them , If the lock is occupied for a short time, then spin is very valuable , It will save a lot of time , But on the contrary , If the lock takes a long time , The spinning thread will consume processor resources in vain , It's a waste of performance .
  • So there has to be a limit to spinlocks , That's how many times it spins , Set a spin number , If this number is exceeded, the thread is suspended in the traditional way instead of self rotation ,
  • The number of spins is ten by default . But we can also go through -XX: PreBlockSpin Parameters come from defining settings

Adaptive spinlock

  • We know that we can customize the number of spins , But it's hard to have a reasonable value for this , After all, there are all kinds of situations in the program , We can't set a global . So in JDK6 After that, an adaptive spin lock is introduced , That is to optimize the original spin lock
  • The time of spin is no longer fixed , It is determined by the previous spin time on the same lock and the state of the lock owner , If it's on the same lock object , Spin wait has just successfully acquired a lock , And support locked threads running , Then the virtual machine will be tasked, and this spin will get the lock again , That would allow the spin to last longer
  • Corresponding , If for a lock , The number of times spin gains lock is very small , Then when you want to acquire the lock, you will directly ignore the spinning process, and then directly block the thread to avoid wasting processor resources

Lightweight lock

  • Lightweight locks are also JDK6 A new locking mechanism is added when the , It's lightweight compared to traditional locks implemented by operating system mutexes , Lightweight locks are also an optimization , Instead of replacing heavyweight locks , The original intention of lightweight lock is to reduce the performance consumption of traditional heavyweight locks using operating system Mutex without multithreading competition .
  • To understand lightweight locks, we have to have objects in Heap We know the distribution in , That is to say, the content mentioned above .

Lightweight lock plus lock

  • When code is executed to a synchronized block of code , If the synchronization object is not locked, the lock flag bit is 01 state , Then the virtual machine will first create a record named lock in the stack frame of the current thread Lock Record Space
  • This lock record space is used to store the current Mark Word A copy of the , The official added a Displaced The prefix of , namely Displaced Mark Word , As shown in the figure below , This is CAS State of stack and object before operation
    CAS State of stack and object before operation
  • When replication ends, the virtual opportunity passes through CAS The operation attempts to put the object's Mark Word Update to point Lock Record The pointer to , If the update is successful, it means that the thread owns the lock of the object , And will Mark Word The lock flag bit ( The last two bits ) Turn into “00”, This means that the object is in a lightweight locked state , The state of the stack and object header is as follows :
     The state of the stack and object header
  • If the above operation fails , That means that at least one thread competes with the current thread for the lock on the object , The virtual opportunity first checks the object's Mark Word Whether to point to the current thread's stack frame , If it is , Indicates that the current thread already has a lock on this object , Then directly enter the synchronous code block to execute . Otherwise, the object has been preempted by other threads .
  • If there are more than two threads competing for the same lock , Then lightweight locks are no longer valid , Must inflate to a heavyweight lock , The tag bit of the lock becomes “10”, here Mark Word Stored in is a pointer to a heavyweight lock , The waiting thread must also be blocked

Unlocking of lightweight locks

  • Lightweight locks are also unlocked by CAS operations
  • If the object's Mark Word Still point to the thread's lock record , Then use CAS The operation sets the object's current Mark Word And copied in threads Displaced Mark Word Replace it with
  • If the replacement is successful, the whole synchronization process ends , If it fails, it indicates that another thread is trying to acquire the lock , That's when you release the lock , Wakes up the suspended thread

Lightweight locks are suitable for most of the locks in the whole synchronization cycle without competition , Because if there's no competition , Lightweight locks can be passed through CAS Successful operation avoids the cost of using mutex , But if there is lock competition , In addition to the cost of the mutex itself, it has to happen CAS The cost of the operation , In this case, it's slower than a heavyweight lock

  • The following is a complete flow chart to directly watch the lightweight lock locking and expanding process



Biased locking

  • Biased lock is also JDK6 A lock optimization technique is introduced , If the lightweight lock is passed without competition CAS The operation eliminates the mutex used by synchronization , Then biased locking eliminates the entire synchronization without competition , even CAS The operation is no longer done , As you can see, it's lighter than lightweight locks
  • From the distribution of object heads , There is no hash value in the biased lock, but there are more threads ID And Epoch Two content
  • Biased locking means that the lock is biased toward the first thread that gets it , If the lock has not been acquired by other threads during the next execution , Then only lock biased threads will never need to synchronize again

Biased lock acquisition and revocation

  • When code is executed to a synchronized block of code , The first time it is executed by a thread , The lock object is first acquired by thread , At this point, the virtual machine changes the lock flag in the object head to “01”, At the same time, change the bias lock flag bit to “1”, Indicates that the current lock object is in biased lock mode .
  • Next, the thread passes through CAS Operation to put the frame of the thread ID Record to object header , If CAS succeed . Then the thread holding the lock object enters the synchronization code and does not perform any synchronization operation ( Such as obtaining lock and unlocking ). Each time, it determines the current thread and the thread recorded in the lock object id Is it consistent .
  • If Aforementioned CAS Operation failed , That means there must be another thread getting the lock , And it was successful . In this case, there is lock competition , Then the biased pattern ends immediately , Partial lock revocation , Need to wait for global security ( There is no bytecode executing at this point in time ). It first pauses the thread that owns the biased lock , According to whether the lock object is in the locked state, whether or not to cancel the bias is determined, that is, to change the biased lock flag bit to “0”, If you undo it, it becomes unlocked (“01”) Or lightweight locks (“00”)
  • If the lock object is not locked , Then remove the biased lock ( Set the bias lock flag bit to “0”), At this time, the lock is not locked and cannot be biased , Because it has a hash value , It turns into a lightweight lock
  • If the lock object is still locked, it will enter the lightweight lock state directly

Bias lock switch

  • Bias locked in JDK6 And then it's enabled by default . Due to biased locking, it is suitable for the scenario of lock free competition , If all the locks in our application are normally competitive , Can pass JVM Parameters Close the deflection lock :-XX:-UseBiasedLocking=false, Then the program will enter the lightweight lock state by default .
  • If you want to open the bias lock, you can use : -XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0

Heavyweight lock

  • Heavyweight locks, that is, after the above optimizations are invalid , Inflate to a heavyweight lock , To achieve by mutex , Let's first look at the following code

  • The above code is a simple to use synchronized Code for , We can see the right window through bytecode tool . We found that , Before and after the synchronization code block formed monitorenter and monitorexit Two instructions
  • stay Java There will be one in the present monitor The monitor , there monitorenter The instruction is to get the monitor of an object . And the corresponding monitorexit To release the monitor monitor The ownership of the , Allow access by other threads
  • monitor It's system dependent MutexLock ( The mutex ) To achieve , When a thread is blocked, it enters the kernel state , It will cause the system to switch between user mode and kernel mode , Which in turn affects performance

summary

  • The above is about synchronized Some optimization and conversion of locks , When we turn on the bias lock and spin , The transformation of the lock is unlocked -> Biased locking -> Lightweight lock -> Heavyweight lock ,
  • Spin lock is actually a kind of lock competition mechanism , Not a state . Spin is used in both biased and lightweight locks
  • Biased locking is suitable for the scenario of lock free competition , Lightweight locks are suitable for scenarios without multiple thread contention
  • Both biased and lightweight locks depend on CAS operation , But in biased locks, only the first time CAS operation
  • When an object has been computed a consistent hash , Then the object can no longer enter the biased lock state , If the object is in a lock biased state , And receive a request to calculate the hash value , Then his biased lock state will be revoked immediately , And it will inflate into a heavyweight lock . If that's why it's locked MarkWord There is no hash value in

This paper is written by AnonyStar Release , It can be reproduced, but the source of the original text should be stated .
Welcome to the wechat public account : Yunqi code Get more quality articles
More articles focus on the author's blog : Yunqi code i-code.online

版权声明
本文为[AnonyStar]所创,转载请带上原文链接,感谢