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 bysynchronized
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 instancesynchronized
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 seesynchronized(lock)
Is based onlock
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 toi-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 calledMark 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 realizeMark Word
Code inmarkOop.cpp
in , We can see the following clip , This is a description of the virtual machineMarkWord
Storage layout of :
- When we're in
new
When an object , The virtual machine layer actually creates ainstanceOopDesc
object , Familiar to usHotspot
Virtual machine usesOOP-Klass
Model to describeJava
Object instances , amongOOP
It's the familiar common object pointer , andKlass
It describes the specific types of objects , stayHotspot
We useinstanceOopDesc
andarrayOopDesc
To describe , amongarrayOopDesc
Used to describe array types , - about
instanceOopDesc
We can realize it fromHotspot
Source code found in . Corresponding toinstanceOop.hpp
In file , And the correspondingarrayOopDesc
stayarrayOop.hpp
in , Let's take a look at the relevant content :
- We can see
instanceOopDesc
InheritedoopDesc
, andoopDesc
It is defined inoop.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 ismarkOopDesc
- Through code follow-up, we can find
markOopDesc
In the definition ofmarkOop.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 , Whensynchronized
When an object is used as a lock, then the information of the following series of locks is the same asmarkOop
relevant . As in the table abovemark word
The distribution record shows the meaning of the specific parts - Because we're actually creating objects in jvm Layers will generate a
native
Ofc++
objectoop/oopdesc
To map , And every object has amonitor
Monitor object for , Can be inmarkOop.hpp
see , In fact, in multithreading, seizing the lock is just fighting formonitor
To modify the corresponding tag
Synchronized In depth
- stay
Java
insynchronized
Is the most basic method to achieve mutual exclusion synchronization , It's a block structure (Block Structured
) Synchronization syntax of , afterjavac
After compiling, it will be formed before and after the blockmonitorrenter
andmonitorexit
Two bytecode instructions , And they all need onereference
Type to indicate the lock object , The specific lock object depends onsynchronized
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 inJava
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 , Sosynchronized
It's a massive operation , But in thejdk6
After that, a lot of optimization has been done , Make it less heavy
Lock optimization
- stay
JDK5
Upgrade toJDK6
After a series of lock improvements , Optimize locks through a variety of techniques , Give Waysynchronized
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 isfinal
Decorated immutable class , So the addition of strings is done by generating newString
Let's try the first one , So the compiler optimizes this operation , stayJDK5
It will be converted toStringBuffer
Object'sappend()
operation , And in theJDK5
And then it turns intoStringBuilder
Object to operate . So the above code is injdk5
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 issb
, At this time, the virtual machine through the analysis found thatsb
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 successionappend
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 oneappend
From before to lastappend
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 , stayjdk4
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 threadLock Record
Space - This lock record space is used to store the current
Mark Word
A copy of the , The official added aDisplaced
The prefix of , namelyDisplaced Mark Word
, As shown in the figure below , This isCAS
State of stack and object before operation
- When replication ends, the virtual opportunity passes through
CAS
The operation attempts to put the object'sMark Word
Update to pointLock Record
The pointer to , If the update is successful, it means that the thread owns the lock of the object , And willMark 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 :
- 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 useCAS
The operation sets the object's currentMark Word
And copied in threadsDisplaced 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 happenCAS
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 competitionCAS
The operation eliminates the mutex used by synchronization , Then biased locking eliminates the entire synchronization without competition , evenCAS
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 , IfCAS
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 formedmonitorenter
andmonitorexit
Two instructions - stay Java There will be one in the present
monitor
The monitor , theremonitorenter
The instruction is to get the monitor of an object . And the correspondingmonitorexit
To release the monitormonitor
The ownership of the , Allow access by other threads monitor
It's system dependentMutexLock
( 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 timeCAS
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