The fundamentals and challenges of multithreading
Thinking triggered by a question
The proper use of threads can improve the processing performance of programs in two main ways
- The first is to use multi-core CPU and hyperthreading technology to achieve parallel execution of threads;
- The second one is asynchronous execution of the thread compared to synchronous execution, asynchronous execution is a great way to optimize the processing performance of the program and improve the concurrent throughput but also causes a lot of trouble
Multithreading brings security issues to shared variable access
A variable i. If a thread accesses this variable to modify it, there is no problem with data modification or access. But if multiple threads make changes to the same variable, there is a data security issue
For thread-safety, access to data state is essentially managed, and this state is usually shared and mutable.
- Shared, which means that the data variable can be accessed by multiple threads;
- Mutable means that the value of a variable can change over its lifetime.
Whether an object is thread-safe depends on whether it can be accessed by multiple threads and how the object is used in the program. So if multiple threads access the same Shared object, without additional synchronization and invoke server-side code under the condition of without doing other coordination, the Shared object’s state is still correct (correctness means that the results of this object to keep consistent with the result we expected), that means the object is thread-safe.
Thinking about how to ensure data security in parallel threads
The essence of the problem is concurrent access to shared data. If there was a way to serialize parallel threads, wouldn’t that be a problem? So, the first thing that comes to mind should be the lock. After all, this scenario is not unfamiliar, and database dealing with the time, we have understood the concept of pessimistic lock, optimistic lock.
What is a lock?
It is a synchronous means of handling concurrency, and the lock must be mutually exclusive if it is to achieve one of the goals described above. The locking method provided by Java is the Synchroinzed keyword.
synchronized
Basic understanding of
Synchronized has long been an elder statesman in multithreaded concurrent programming, and many would call it a heavyweight lock. However, with various optimizations made to Synchronized in Java SE 1.6, biased and lightweight locks introduced in Java SE 1.6 to reduce the performance cost of acquiring and releasing locks are less heavy in some cases.
synchronized
Basic syntax of
Synchronized has three ways of locking
- Modifier instance method, used to lock the current instance, before entering the synchronization code to obtain the current instance lock
- Static method that locks the current class object before entering the synchronization code
- Modifies a block of code that specifies a lock object, locks a given object, and acquires the lock for the given object before entering a synchronized code base.
Different modifier types, representing the control granularity of the lock
synchronized
The application of
public class Demo { private static int count = 0; public static void inc() { synchronized (Demo.class) { try { Thread.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } count++; } } public static void main(String[] args) throws InterruptedException { for (int i = 0; i < 1000; i++) { new Thread(() -> Demo.inc()).start(); } Thread.sleep(3000); System.out.println(" run result "+ count); }}Copy the code
Think about how locks are stored
You can think about it for a moment, in order to implement multi-threaded mutual exclusion, what factors does this lock need?
- A lock needs to have something to represent it, like what is the state of being locked, what is the state of being unlocked
- This state needs to be shared with multiple threads
How are synchronized locks stored? Observing the whole syntax of synchronized, it is found that synchronized(lock) controls the lock granularity based on the life cycle of the lock object. Is there any relationship between the storage of the lock and the lock object? Starting with how the object is stored in JVM memory, we can see what features the object has that enable locking.
Layout of objects in memory
In the Hotspot virtual machine, the object storage layout can be divided into three areas: object Header, Instance Data, align Padding.
Explore the Jvm source code implementation
When we areJava
In the code, usenew
When creating an object instance, (hotspot
The virtual machine)JVM
The layer actually creates oneinstanceOopDesc
Object.Hotspot
Vm useOOP-Klass
Model to describeJava
Object instance,OOP(Ordinary Object Point)
Refers to ordinary object Pointers,Klass
Used to describe the specific type of an object instance.Hotspot
usinginstanceOopDesc
和 arrayOopDesc
To describe the object header,arrayOopDesc
The object represents an array typeinstanceOopDesc
In the definition ofHotspot
In the sourceinstanceOop.hpp
In the file, in addition,arrayOopDesc
Corresponds to the definition ofarrayOop.hpp
.
From the instanceOopDesc code, you can see that instanceOopDesc is derived from oopDesc, whose definition is in the oop. HPP file in the Hotspot source code. In the ordinary instance object, the definition of oopDesc contains two members, _mark and _metadata, respectively
_mark
Represents the object tag and belongs tomarkOop
Types, that’s what we’re going to talk aboutMark World
Which records information about the object and the lock_metadata
Represents class meta information, which stores the class metadata to which an object refers (Klass
), whereKlass
Represents a common pointer,_compressed_klass
Represents a compressed class pointer
MarkWord
inHotspot
,markOop
In the definition ofmarkOop.hpp
In the file, the code is as followsMark Word records information about objects and locks. When an object is treated as a lock by the synchronized keyword, a series of operations around the lock are related to Mark Word. The Mark Word length is 32bit on a 32-bit VM and 64bit on a 64-bit VM. The data stored in Mark Word will change with the change of the lock flag bit. Mark Word may change to store in the following 5 situations
Why can any object be locked
-
First, every Object in Java is derived from the Object class, and every Java Object has a native C++ Object oop/oopDesc that corresponds within the JVM.
-
When a thread acquires a lock, it actually acquires a monitor object (
monitor
),monitor
You can think of it as a synchronization object, all of itJava
The object is born to carrymonitor
. inhotspot
Source,markOop.hpp
In the file, you can see the following code.When multiple threads access a block of synchronized code, it is equivalent to going to the scramble ObjectMonitor to modify the lock identifier in the object. In the above code, the ObjectMonitor object is closely related to the thread scramble lock logic
Upgrades to synchronized locks
In analyzing Markword, biased locks, lightweight locks, and heavyweight locks are mentioned. In analyzing the differences between these types of locks, let’s first consider the problem that using locks can achieve data security, but will bring performance degradation. Not using locks improves performance based on thread parallelism, but does not guarantee thread-safety. There seems to be no way between the two to meet both performance and security requirements.
The authors of the hotspot VIRTUAL machine have investigated and found that, in most cases, locking code is not only not contested by multiple threads, but is always acquired multiple times by the same thread. Based on this probability, synchronized made some optimizations after JDK1.6, introducing the concept of biased locking and lightweight locking in order to reduce the performance cost of acquiring and releasing locks.
Therefore, in synchronized, there are four states of lock: no lock, biased lock, lightweight lock and heavyweight lock. The lock status escalates from low to high depending on the level of competition.
The basic principle of bias locking
As mentioned earlier, in most cases, locks are not only not contested by multiple threads, but are always acquired multiple times by the same thread. In order to make the cost of acquiring locks lower, the concept of biased locking is introduced.
How do we understand biased locking? When a thread accesses a block of code with a synchronized lock, the ID of the current thread is stored in the object header, and the thread subsequently enters and exits the block without having to re-lock and release the lock. Instead, it directly compares whether the bias lock to the current thread is stored in the object header. If equality means that the biased lock is biased in favor of the current thread, there is no need to try to acquire the lock
Biased lock acquisition and undo logic
-
Firstly, obtain the Markword of the lock object and judge whether it is in the biased state. (biased_lock=1 and ThreadId is empty)
-
If it is biased, the ID of the current thread is written to MarkWord via CAS
- A) if
cas
Success, thenmarkword
It’s going to look like this. Indicates that the biased lock of the lock object has been acquired, and then executes the synchronized code block - B) if
cas
Failure indicates that another thread has acquired a biased lock. In this case, the current lock is contested, and the thread that has acquired a biased lock needs to be revoked and its lock upgraded to a lightweight lock (this operation can only be performed until the global safety point, i.e., no thread is executing bytecode)
- A) if
-
If it is biased, you need to check whether the ThreadID stored in MarkWord is the same as the ThreadID of the current thread
- A) If the lock is equal, it does not need to acquire the lock again and can execute the synchronized code block directly
- B) If not, it indicates that the current lock is biased to other threads, and the biased lock needs to be revoked and upgraded to a lightweight lock
Bias lock revocation
The cancellation of biased lock is not to restore the object to the no-lock-biased state (because biased lock does not have the concept of lock release), but to directly upgrade the biased lock object to the state with lightweight lock when the CAS failure is found in the process of acquiring biased lock. When the thread holding the biased lock is revoked, the thread that obtained the biased lock has two situations:
- If the thread that acquired the biased lock has exited the critical section, that is, the synchronization block has finished executing, then the object header is set to lock free and the thread that grabs the lock can be based on
CAS
Re-bias but before the thread - If the original get biased locking thread synchronization code block is not performed, in the critical zone, this time will win the biased locking the original thread after the upgrade for lightweight lock continue synchronized code block in our application development, most of the time there will be two or more threads competition, if open the biased locking, Instead, it increases the resource cost of acquiring locks. So you can go through
jvm
parameterUseBiasedLocking
To turn bias locking on or off
Flow chart analysis
The fundamentals of lightweight locks
Locking and unlocking logic for lightweight locks
After the lock is upgraded to a lightweight lock, the object’s Markword is changed accordingly. Upgrading to a lightweight lock process:
- The thread creates lock records in its own stack frame
LockRecord
. - Locks the object header of the object
MarkWord
Copy to the lock record just created by the thread. - Will lock records in
Owner
Pointer to lock object. - Locks the object header of the object
MarkWord
Replace with a pointer to the lock record.
spinlocks
Lightweight lock in the lock process, the use of spin lock. The so-called spin means that when another thread is competing for the lock, the thread will wait in the loop, rather than blocking the thread until the thread that acquired the lock releases the lock, the thread can immediately acquire the lock. Note that locking in an in-place loop consumes CPU, which is equivalent to executing a for loop with nothing at all. Therefore, lightweight locks are suitable for scenarios where synchronized blocks of code execute quickly, so that threads wait in place for a short time before acquiring the lock. Spin-locks are used in a probabilistic context, where most synchronized blocks of code execute for a very short time. So the performance of the lock can be improved by seemingly meaningless loops. But the spin must be conditional, otherwise if a thread executes a block of synchronized code for a long time, the thread’s constant loop will consume CPU resources. The default number of spins is 10, which can be modified with preBlockSpin
After JDK1.6, adaptive spin locking was introduced, which means that the number of spins is not fixed, but determined by the time of the previous spin on the same lock and the state of the lock owner. If the spin wait has just successfully acquired the lock on the same lock object, and the thread holding the lock is running, the virtual machine will assume that the spin wait is likely to succeed again, and it will allow the spin wait to last a relatively long time. If spin is rarely successfully acquired for a lock, it is possible to omit the spin process and block the thread directly in future attempts to acquire the lock, avoiding wasting processor resources.
Unlock lightweight locks
The lock release logic of lightweight lock is actually the reverse logic of acquiring lock. Through CAS operation, the LockRecord in thread stack frame is replaced back to the MarkWord of lock object. If successful, there is no competition. If it fails, it indicates that the current lock is competing, and the lightweight lock will swell to become a heavyweight lock.
The fundamentals of heavyweight locking
When the lightweight lock expands beyond the heavyweight lock, it means that the thread has to be suspended and blocked waiting to be awakened.
Heavyweight lockmonitor
After the synchronization block is added, you will see a Monitorenter and Monitorexit in the bytecode. Each JAVA object is associated with a monitor, which we can think of as a lock. When a thread wants to execute a synchronized method or block of code modified by synchronized, The thread must first obtain the monitor corresponding to the synchronized modified object. Monitorenter means to get an object monitor. Monitorexit frees ownership of the Monitor so that other blocked threads can try to acquire it. Monitor relies on the MutexLock of the operating system to implement it. After a thread is blocked, it enters the kernel scheduling state. This will cause the system to switch back and forth between the user state and the kernel state, which seriously affects the lock performance.
Heavyweight lock the basic process of the lock
Arbitrary thread pairObject
(Object
由 synchronized
Protect) access, first to obtainObject
The monitor. If the fetch fails, the thread enters the synchronization queue and the thread state changes toBLOCKED
. When accessingObject
The release wakes up the thread blocking in the synchronization queue to try again to acquire the monitor.
Review the thread contention mechanism
Let’s review some of the basic flow of thread contention for lock escalation. To make things easier to understand, there is a block of synchronized code, Thread#1, Thread#2, etc
synchronized (lock) {
// do something
}
Copy the code
- Situation one: Only
Thread#1
Will enter the critical region; - Situation 2:
Thread#1
和Thread#2
Enter the critical area alternately, the competition is not fierce; - Case 3:
Thread# 1/2 / Thread3 Thread#...
At the same time into the critical area, fierce competition;
Biased locking
When Thread#1 enters the critical zone, the JVM sets the lock flag bit of the lockObject header Mark Word to “01”, and records the thread ID of Thread#1 into the Mark Word with the CAS operation. “Biased” means that the lock is biased in favor of Thread#1, and if no other thread enters the critical section later, Thread#1 does not need to perform any synchronization operations. In other words, if only Thread#1 enters the critical region, in fact only Thread#1 needs to perform CAS when it enters the critical region for the first time, and there is no synchronization overhead when it enters the critical region again.
Lightweight lock
More often than not, Thread#2 will try to enter the critical section. If Thread#2 also enters the critical section but Thread#1 has not finished executing the synchronized code block, it will suspend Thread#1 and upgrade to a lightweight lock. Thread#2 spins to try again to acquire the lock as a lightweight lock.
Heavyweight lock
If Thread#1 and Thread#2 are executed alternately, then the lightweight lock will suffice. However, if Thread#1 and Thread#2 enter the critical section at the same time, the lightweight lock will inflate to the heavyweight lock, meaning that Thread#2 will block if Thread#1 has acquired the heavyweight lock
Synchronized
In combination withJava Object
The object of thewait,notify,notifyAll
Earlier in synchronized, detection of when a blocked thread is awakened depends on when the thread that acquired the lock finishes executing the synchronized block and releases the lock. So how do you do display control? In Object pairs, wait, notify, and NotifyAll are used to control the state of threads.
wait/notify/notifyall
The basic concept
-
Wait: Thread A, which holds the object lock, intends to release the object lock permission, release CPU resources, and enter the waiting state.
-
Notify: Indicates that thread A, which holds the object lock, intends to release the object lock permission, notifying the JVM to wake up A thread X, which is competing for the object lock. After thread A synchronizes and releases the lock, thread X directly obtains the object lock permission, and other competing threads continue to wait (even after thread X completes synchronization and releases the object lock, other competing threads still wait until A new notify and notifyAll are called).
-
NotifyAll: The difference between notifyAll and notify is that notifyAll wakes up all the threads competing for the same object lock. After thread A, which has obtained the lock, releases the lock, all the awakened threads may obtain the object lock permission
Note that: Three methods have to be in a synchronized synchronous calls, in the scope defined by keyword or complains. Java lang. IllegalMonitorStateException, meaning because no synchronization, so the thread on the state of the object lock is uncertain, can’t call these methods. In addition, synchronization is used to ensure that the notify thread is aware of changes made to variables when it returns from the WAIT method.