Writing in the front
The occurrence of lock is required by multi-threaded concurrent programming. If the program is executed concurrently and operates on a resource at the same time, it is easy to have problems: Multiple threads running at the same time, like creatures living in different dimensions on the same earth, they don’t know each other, but they’re operating on the same thing, maybe operating on the same thing, and all of a sudden it’s gone, or there’s more of it. This is because they operate simultaneously and without telling each other. Java has two native locking mechanisms, one through the underlying synchronized keyword and the other through the Lock class in the java.util.concurrent package implemented by Doug Lea in JDK1.5. These two methods, one is Java keywords, and the other is the way to use objects, both of which implement concurrent locking of public resources.
Principle of synchronized
Synchronized is a Java keyword that locks concurrent resources and is implemented by the JVM, meaning that synchronized has a low-level relationship. The synchronized keyword has been around since JDK 1.0, starting as an expensive thread-safe method (but the only one), and was redesigned in JDK 6 for much improved performance. This performance improvement can be attributed to advances in software code design as well as hardware development.
The original synchronized keyword
The original synchronized keyword, based on the principle of mutually exclusive synchronization to achieve. Mutual-exclusive synchronization means that if one thread is using a resource and another thread wants to use it, it has to wait until it can obtain the resource. This is the expression of mutual-exclusive synchronization. If one thread uses the resource, the other is not allowed to use it (i.e. blocked). Mutex synchronization is a performance-intensive operation because of the way the mutex is implemented: blocking.
The user and kernel states of the operating system are mentioned here. Mainstream Java virtual machine for the implementation of Java threads, is directly map Java threads to the operating system’s native kernel threads, so the implementation of thread blocking and thread arousal, the operating system must help complete the conversion between the user state and the kernel state of the operating system. When a thread attempts to acquire a resource, the thread blocks. The blocking is done with the help of the operating system, which moves from user mode to kernel mode and blocks the thread in kernel mode.
User-state and kernel-state are very important operating system concepts, which I will not study here, just remember that there is such a thing. Context information needs to be stored during the transition between user and kernel states, which is very resource-consuming. Therefore, mutually exclusive synchronization is very costly, and switching between user and kernel states consumes processor time even longer than synchronized code execution. This is a very heavyweight operation, which contributed to the poor performance of the original synchronized keyword.
Improved synchronized keyword
The reason for the poor performance of synchronized keyword at first is that mutex synchronization is realized through thread blocking, and thread blocking inevitably leads to the conversion between user state and kernel state in the operating system, so the performance is poor. Performance might be better if the synchronized keyword were not implemented through mutex (not safe by blocking threads).
Blocking is an option because without blocking threads, the process of manipulating data cannot be guaranteed to be safe. The most common phenomenon of insecurity is that one thread has read data, and before saving, another thread has modified the data, so that when saving, the data will be ignored. In other words, the operation of the other thread is “invalidated”.
A common method is to record the data value when obtaining resources, and check whether the data is still the current size when saving. If so, the default resource has no problem and can be saved. This is a very important concept in concurrency: CAS (compare and swap – swap), which compares the expected data first, and swaps the value if the expected size (save). It is worth mentioning that CAS is another way to achieve thread-safety: the core logic of the JUC package. (But this is actually potentially problematic. For example, if I get the data at 10am and know it is 1, and find it at 5pm, it is still 1. This does not guarantee that the data has not been changed during this time. Fortunately, ABA problems do not affect concurrency in most cases.
If you manipulate data through CAS, you can replace blocking and improve performance. The original name of CAS is compare & Swap, which means that compare and swap must be performed together. After compare is performed, swap must be performed, that is, the two actions together are atomic and cannot be separated. This is why synchronized keyword performance is improved by hardware technology, because CAS must be implemented by hardware, not software (if it is implemented by software, it is implemented by mutex, which makes no sense). The original CPU did not have CAS operations in the hardware instruction set until later, when Java libraries in JDK 5 began to use CAS operations, which were used in JDK 6 to revamp synchronized.
Roughly speaking, the principle of synchronized transformation through CAS operation is divided into two situations: if only one thread uses resources (but it is theoretically possible for other threads to grab resources), CAS can directly save data without blocking the thread; If more than one thread is competing for resources, there is no way to block the thread obediently and achieve thread-safety through mutually exclusive synchronization.
supplement
The original synchronized achieves thread safety through mutual exclusion, while the new synchronized partially achieves thread safety through CAS operation, which is actually two ideas in the face of concurrency risks.
- The idea behind mutex synchronization is that there is a risk of concurrency, so I’m prepared to use it in one thread and not in the other.
- The idea of CAS is that concurrency risks may occur, so there is no need to prepare in advance. CAS operation should be performed first to save the data, and then the data should be found to be different.
One is to deal with risks in advance and nip them in the cradle; the other is to operate first regardless of risks and take compensation measures when conflicts arise. These two ideas are actually the “optimistic locking” and “pessimistic locking” ideas in the locking mechanism. Optimism and pessimism refer to attitudes towards concurrency risks:
- Optimistic words, regardless of the risk, do it again, if there is a problem, come back to compensate (corresponding to CAS operation)
- In the pessimistic case, consider the risk first, safe, and then perform data processing (corresponding to mutex synchronization)
So optimistic locks rollback retry, pessimistic locks block transactions. Synchronized is the synchronized keyword after JDK 6.
Principle of synchronized
Learning the synchronized keyword requires an understanding of the memory layout of objects in the JVM, especially the object header. The contents of the object header will not be described here.
Objects are stored in the JVM heap, and given the cost of memory, object headers need to be as small as possible, so there are five states of object headers that store different information in each state. The first four of the above are related to the synchronized keyword, respectivelyNo lock, Biased lock, Lightweight lock, and Heavyweight lock
State stores different information. To put it another way, this means that synchronized also has four scenarios.
Synchronized is the (improved) equivalent of putting a car in gear, going into first gear, picking up speed, going into second gear, and finally stepping into third.
- If only one thread is using the resource, attach one gear: bias lock
- If a few threads are using the resource, then suspend the second gear: lightweight lock
- If more than one thread is using the resource, then hold the third block: the heavyweight lock
These three gears are synchronized after JDK 6, starting directly in third gear before that.
Corresponding to these three gears (plus neutral gear), there are altogether four states, the symbol bits of these four states are as follows:
The lock | Bias mode (1 bit) | Lock flag bit (2 bits) |
---|---|---|
unlocked | 0 | 01 |
Biased locking | 1 | 01 |
Lightweight lock | (Does not have this field) | 00 |
Heavyweight lock | (Does not have this field) | 10 |
Several state locks
Biased locking
For locked objects, there is a scramble for resources to upgrade. At the beginning, there is only one thread using the resource, so there is no race. If there is no competition, there is no need to lock, or lock heavyweight, because there are no other threads competing for resources.
Biased lock bias, is “biased”, “biased”, its meaning is biased to the thread, biased to the first thread to obtain it. If no other thread ever appears, the thread holding the biased lock never needs to synchronize. If a new thread appears, the bias lock is terminated immediately.
Therefore, if only one thread uses the resource, bias locks are used. If there is a second thread, regardless of whether the two threads are competing, the lock expands and the bias lock is immediately invalidated. In this sense, biased locks do not need to be unlocked, because there is only one owner of the lock from the beginning to the end, and when there is a second owner, it becomes invalid. Not unlocking is one of the differences between biased locks and lightweight and heavyweight locks.
The concrete realization of bias lock is actually quite tedious. Generally speaking, the thread ID of the biased thread is recorded in the object header, and then the thread ID is matched before use. If it is the current thread, no synchronization is required. If it is not the current thread, then the biased lock is stopped immediately.
In detail, the locking process of bias lock is as follows (refer to the object header diagram above by yourself) :
Make sure you can tilt the lock up
First, the object should be unlocked (the lock flag bit is 01), and the object should be biased (the bias flag bit is 1), so the mark part of the object header should end with 101. Since the lock flag bit of no-lock and biased lock is the same (both is 01), another 1 bit is used to indicate whether the object can be biased. Bias locking is enabled on the HotSpot VIRTUAL machine in JDK 6 by default. You can manually set the parameters to disable bias locking.
In fact, this is not necessarily the case. If the Object has not computed the hashcode (for example, calling Object :: hashcode () computed the hashcode), the hashcode will not be stored in the Object header. But once the hash code has been calculated, the hash code is stored in the object’s header, and the object never goes into a biased lock state again. If it needs to be locked, it just expands into a heavyweight lock in one step.
(attach64The bit JVM's object header marks the field, in the lock-free and lock-biased states: ) | -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - | -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - | | unused:25 | identity_hashcode:31 | unused:1 | age:4 | biased_lock:1 | lock:2| | Normal | (unlocked) -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - | -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - | | thread:54 | epoch:2 | unused:1 | age:4 | biased_lock:1 | lock:2| Biased | | (Biased locking) -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - | -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - |Copy the code
Try up bias locking via CAS
After ensuring that the object is in bias mode, the JVM uses CAS to record the ID of the thread that acquired the lock in the object’s tag field (the object header tag field, in bias mode, is 23 bits for 32-bit VMS and 54 bits for 64-bit VMS). If the CAS records that the thread ID is successful, the biased lock is considered to have been locked successfully. In the future, the thread holding the biased lock does not need to perform any synchronization operation every time it enters the lock related synchronization block. If CAS fails to record the thread ID, the bias mode ends immediately.
- If the object is not locked, then the object will first undo bias (with the bias flag set to 0) and then upgrade to lightweight lock (this step has a performance cost).
- If the object has a bias lock at this point, the object continues to apply for a lightweight lock
(Below is an illustration from Understanding the Java Virtual Machine, which describes the process of ballooning biased locks to lightweight locks.)
There is another field in the tag field of the object header: bias timestamp (EPOCH)
This field counts the number of rebias. The concept of heavy bias is that if you have a class that instantiates 20 objects, those 20 objects go through thread 1, then thread 2, and then you have to undo bias 20 times to get to a lightweight lock. Biased locking is expensive to undo, and if this happens more than once, it means the class is not suitable for biased locking.
The JVM is individually optimized for this scenario, where the class records an EPOCH value and the object will have an EPOCH value when it is created (the same as the class). If a large-scale revocation of bias occurs on a class object, the epoch value of the class will be increased by one (future objects will also adopt the new EPOCH value). If the epoch value of the class exceeds a certain threshold, it is proved that the class is not suitable for biased locking, and the future objects will not use biased locking, but will directly use lightweight locking.
The epoch value in the object header is used to compare with the class’s epoch value, and if it is different, it is inflated directly to the lightweight lock.
Lightweight lock
When a resource is acquired by two or more threads instead of just one, the bias lock is immediately invalidated and ballooned to a lightweight lock.
The meaning of lightweight lock is that if multiple threads acquire resources alternately, without the risk of resource contention, then add a lightweight lock to ensure that one thread does not operate in parallel while the other thread is running. Therefore, the purpose of lightweight lock is to eliminate synchronization primitives in the case of uncontested data and improve the running performance of the program.
(attach64The bit JVM's object header marks the fields in the biased and lightweight lock states: ) | -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - | -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - | | thread:54 | epoch:2 | unused:1 | age:4 | biased_lock:1 | lock:2| Biased | | (Biased locking) -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - | -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - | | ptr_to_lock_record | lock:2| Lightweight Locked | (Lightweight lock) |---------------------------------------------------------------------|--------------------|Copy the code
The lock flag bit for both no-lock and biased locks is 01 and can be expanded to a lightweight lock by changing the lock flag bit to 00. For lightweight locks, the tag field of the locked object header has only two parts: the lock flag bit (2 bits, 00) and the ID of the thread that is holding the object.
The process of expansion from lock-free to lightweight lock is like this (if it is biased to lock, it should be reversed to the lock-free state first, and then inflated) :
- Ensure that the object is not locked. The lock flag is yes 01.
- Copy the Mark Word field of the object header to the stack frame of the current thread. That is, the 8-byte tag field containing hash code, generational age, bias state, lock flag bits, and so on, is stored in the JVM stack of the current thread. The marker field is stored at the address of the thread stack frame, called a “Lock Record” or, to put it another way, this Lock Record is used to store a copy of the object’s current Mark Word.
- The VIRTUAL machine uses the CAS operation to try to update the Mark Word of the object header to a pointer to the Lock Record address (that is, the thread stack frame backed up the object header address in the previous step) and update the Lock Mark of the object header to a lightweight Lock (00). If this CAS operation succeeds, then the lightweight lock is fine. If not, it proves that there are multiple threads competing for resources at the same time, the lightweight lock is no longer valid, and the lock expands further into the heavyweight lock.
If the object has a lightweight lock, when a thread requests the resource again:
- If it is the same thread, it is a lock reentrant. Each Lock reentrant still creates a Lock Record in the thread stack frame, except that the value of the Lock Record created by the reentrant is null, meaning that it is no longer a backup of the object header marker field.
- If it is another thread, it indicates that there are multiple threads competing for the lock and the lock expands to a heavyweight lock.
The lightweight lock can be unlocked. When the thread is finished manipulating the object resource, the lightweight lock needs to be released. The method of unlocking is to replace the Mark Word of the object header and the Lock Record in the thread stack with CAS. If the CAS operation fails, it means that other threads are competing for resources and the Lock expands.
Heavyweight lock
When two or more threads operate resources at the same time, thread contention will occur. In this case, the lock expands to the strongest heavyweight lock and adopts the mode of mutually exclusive synchronization, so that only one thread operates resources at the same time, and the other threads block and wait.
Heavyweight locks implement mutually exclusive synchronization in multi-threaded contention through a monitor object. Monitor is an important design in concurrent design and has different implementations in different languages. There can only be one Monitor object per class or per object. This monitor is created by the JVM to ensure that only one thread uses the resource at a time and all other threads block.
In JVM, monitor is an instance object of ObjectMonitor class. The source code of this class is written in C++, and the code is as follows:
ObjectMonitor() {
_header = NULL;
_count = 0; / / monitor into the number
_waiters = 0,
_recursions = 0; // The number of threads reentrant
_object = NULL;
_owner = NULL; // Identifies the thread that owns the monitor
_WaitSet = NULL; _WaitSet is the first node in the list
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ; // Multi-threaded contention lock entry when the single necklace table
FreeNext = NULL ;
_EntryList = NULL ; // Threads in the waiting block state are added to the list
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
}
Copy the code
The monitor object instantiated by this class monitors each class or object that requires mutex synchronization on a one-to-one basis. The _owner attribute records the successful concurrent contention threads, and the next thread implements heavyweight locking after execution. If another thread attempts to acquire Monitor, it will be forced to block because the thread reentrant count is not zero.
Synchronized is used as a block of code, for example:
public void func(a) {
synchronized (this) {
// ...}}Copy the code
When the JVM interprets Java code as a CPU primitive, parsing the synchronized keyword interprets the start and end of a block of code as Monitorenter and Monitorexit, respectively, which are very visual primitives. That is, enter monitor and leave Monitor. With these two CPU primitives, the JVM causes each thread to report to Monitor for rescheduling.
Synchronized using
There are four uses of synchronized keyword, namely, synchronized object, class, method and static method. However, synchronized method and static method are actually synchronizing objects and classes. Therefore, synchronized keyword synchronizes objects or classes in principle.
Synchronizing an object
Add synchronized to any object, and the code in the block is synchronized.
Object object = new Object();
synchronized (object) {
// ...
}
Copy the code
For example: Implement a Runnable interface that prints 1-10 in order
// R1 is not synchronized
Runnable r1 = () -> {
for (int i = 1; i <= 10; i++) { System.out.print(i); }};/ / r2 synchronization
Runnable r2 = () -> {
synchronized (object) {
for (int i = 1; i <= 10; i++) { System.out.print(i); }}};Copy the code
At this point, unsynchronized R1 and synchronized R2 are run separately in the thread pool, with two threads running in each thread pool
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.execute(r1);
executorService.execute(r1);
// Print the result: 1 1 2 2 3 4 5 6 7 8 9 10 3 4 5 6 7 8 9 10
executorService.execute(r2);
executorService.execute(r2);
// Print the result: 1 2 3 4 5 6 7 8 9 10 1 2 3 4 5 6 7 8 9 10
Copy the code
A very common way to synchronize an object is to synchronize this in a class method, which means to synchronize the current object.
synchronized (this) {
// ...
}
Copy the code
Synchronizing a class
When synchronized synchronizes a class, all threads that use that class, regardless of which object they are operating on, synchronize.
public void func(a) {
synchronized (SynchronizedExample.class) {
// ...}}Copy the code
For example: define a MyClass class with only one method that prints 1-10 in sequence. Generates objects of two classes and calls two threads to execute the print number methods of each class.
// Create a class that contains methods to print numbers
class MyClass {
void testSync(a) {
for (int i = 1; i <= 10; i++) { System.out.print(i); }}}// Create two class objects
MyClass clazz1 = new MyClass();
MyClass clazz2 = new MyClass();
// Executes a method to print numbers in the thread pool
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.execute(() -> clazz1.testSync());
executorService.execute(() -> clazz2.testSync());
// Print the result: 1 2 3 4 5 1 2 3 4 5 6 7 8 9 10 6 7 8 9 10
Copy the code
Synchronized synchronization between threads occurs when a class method is synchronized to a class (any class can be synchronized).
void testSync(a) {
synchronized (Object.class) {
for (int i = 1; i <= 10; i++) { System.out.print(i); }}}1 2 3 4 5 6 7 8 9 10 1 2 3 4 5 6 7 8 9 10
Copy the code
Synchronizing a method
public synchronized void func (a) {
// ...
}
Copy the code
It operates on the same object. (This is where HashTable is inferior to ConcurrentHashMap because it synchronizes methods, locks the entire object, and is too cumbersome.)
Synchronize a static method
public synchronized static void fun(a) {
// ...
}
Copy the code
It operates on the entire class.