Writing in the front
Today we’ll talk about the different types of Synchronized locks: bias, lightweight, heavyweight, and how they expand between the three Synchronized locks. Let’s start with a picture to sum it up
Know knowledge in advance
Lock upgrade process
There are four types of lock states: unlocked, biased, lightweight, and heavyweight. As locks compete, locks can be upgraded from biased locks to lightweight locks to heavyweight locks (but locks are upgraded in one direction, meaning they can only be upgraded from low to high, with no degradation).
Java object head
Because any object can be used as a lock in Java, there must be a mapping that stores that object and its corresponding lock information (such as which thread currently holds the lock and which thread is waiting). An intuitive method is to use a global map to store the mapping relationship, but there are some problems in this way: the thread safety of map needs to be ensured, and different synchronized will affect each other, resulting in poor performance. In addition, if a large number of objects are synchronized, the map may occupy a large amount of memory. The best thing to do is to store this mapping in the object header, because the object header itself has some HashCode, GC data, so it would be nice to co-store the lock information with this information.
In the JVM, an object has an object header in memory in addition to its own data. For ordinary objects, there are two types of information in the header: the Mark Word and the type pointer. In addition, for an array, there is a record of the length of the array. A type pointer is a pointer to the object of the class to which the object belongs. Mark Word is used to store information about the object such as HashCode, GC generation age, lock status, etc. The length of Mark Word is 32 bits on 32-bit systems and 64 bits on 64-bit systems. In order to store more data in limited space, the storage format is not fixed. On 32-bit systems, the format of each state is as follows:
You can see that the lock information also exists in the object’s Mark Word. When the object is in biasable state, mark Word stores the ID of the biased thread. In Lightweight locked state, Mark Word stores a pointer to the Lock Record in the thread stack. When the state is Heavyweight lock, it is a pointer to a Monitor object in the heap.
Global SafePoint
Safepoint is a term we use a lot in GC, simply because it represents a state in which all threads are suspended.
Biased locking
A thread repeatedly acquires/releases a lock. If the lock is a lightweight lock or a heavyweight lock, it is unnecessary to continually unlock the lock, resulting in a waste of resources. So biased locking is introduced, and biased locking in access to resources will record on resources object when the object is biased towards the thread, biased locking will not take the initiative to release, every time such biased locking into will determine whether the resources towards their own, if it is to their own do not require additional operations, can be directly into the synchronous operation.
Bias lock acquisition process
- Whether the bias lock bit in the access Mark Word is set to 1, whether the lock bit is set to 01 — confirm that it is in the bias state.
- If yes, test whether the thread ID points to the current thread. If yes, go to step (5); otherwise, go to Step (3).
- If the thread ID does not point to the current thread, the lock is contested through the CAS operation. If the competition is successful, set the thread ID in Mark Word to the current thread ID, and then execute (5); If the competition fails, perform (4).
- If the CAS fails to obtain a biased lock, a race occurs. When the safepoint is reached, the thread that acquired the bias lock is suspended, the bias lock is upgraded to a lightweight lock, and the thread blocked at the safepoint continues to execute the synchronization code.
- Execute synchronization code.
Bias lock release
The undo of bias locks is mentioned in step 4 above. Biased lock only when other threads try to compete for biased lock, the thread holding biased lock will release the lock, the thread will not actively release biased lock. To revoke the biased lock, we need to wait for the global safety point SafePoint, which will first suspend the thread A that has the biased lock, and then judge the thread A. There are two cases:
Bulk bias
Why is there batch heavy bias
When only one thread enters the block repeatedly, the performance cost of biased locking is negligible, but when another thread attempts to acquire the lock, it is necessary to wait until safe Point to revoke the biased lock to lock free state or upgrade to lightweight/heavyweight lock. This process is costly, so if the runtime scenario itself has multithreaded contention, biased locking will not improve performance, but will degrade it. As a result, a batch rebias/undo mechanism has been added to the JVM.
Principle of batch heavy bias
- First, we introduce a concept called epoch, which is essentially a timestamp that represents the validity of a biased lock. The epoch is stored in a MarkWord for a biased object. In addition to the epoch in the object, an epoch value is stored in the class information of the object.
- When a global safety point is encountered, such as batch rebias for class C, the epoch stored in class C will be added first to create a new EPOch_new
- Then, all thread stacks holding class C instances are scanned to determine whether the thread has locked the object. Only the value of EPOch_new is assigned to the locked object. That is, the value of epoch_new is assigned to the object that is still used.
- When a thread attempts to acquire a biased lock, it checks whether the epoch stored in class C is equal to the epoch stored in the target object. If the epoch is not equal to the epoch stored in the target object, Epoch_new = epoch_new (epoch_new); epoch_new = epoch_new (epoch_new); epoch_new = epoch_new (epoch_new); The contending thread can then attempt to re-bias the object.
Lightweight lock
The acquisition process of lightweight locks
- When the code enters the synchronization block, if the synchronization object Lock state is biased state (i.e. the Lock flag bit is “01” state, whether the bias Lock flag bit is “1”), the virtual machine will first establish a space called Lock Record in the current thread stack frame, which is used to store the current copy of the Lock object Mark Word. Official called Displaced Mark Word (so here we think that the Lock Record and Displaced Mark Word is the same concept). The state of the thread stack and the object header is shown below:
- Copy the Mark Word from the copy object header to the lock record.
- After the copy is successful, the VM uses the CAS operation to try to change the Mark Word in the object header to a pointer to the Lock Record, and the owner pointer in the Lock Record to the Mark Word in the object header. If the update succeeds, go to Step 4; otherwise, go to Step 5.
- If the update succeeds, the thread owns the lock on the object, and the object’s Mark Word lock bit is set to 00, indicating that the object is in a lightweight locked state. At this point, the thread stack and object header state are as follows:
- If the update operation fails, the vm will check whether the object’s Mark Word points to the stack frame of the current thread. If the update operation fails, the vm will check whether the object’s Mark Word points to the stack frame of the current thread. If the update operation fails, the vm will check whether the object’s Mark Word points to the stack frame of the current thread. It acts as a reentrant counter. The diagram below shows the lock record when reentrant for three times. The lock object is on the left, and the stack frame of the current thread is on the right. After reentrant, it ends. You can then proceed directly to the synchronized block.
If it does not indicate that the lock object has been preempted by other threads, indicating that there are multiple threads competing for the lock, then it will spin to wait for the lock. After a certain number of times, it still does not obtain the lock object, indicating that there is a competition and need to expand to a heavyweight lock.
The process of unlocking lightweight locks
- Attempts to replace the current Mark Word with the product copied in the thread by CAS operation.
- If the replacement succeeds, the synchronization process is complete.
- If the replacement fails, another thread has attempted to acquire the lock (which has ballooned), and the suspended thread must be awakened at the same time the lock is released.
Heavyweight lock
Heavyweight locking locking and locking release mechanism
- call
omAlloc
Assign aObjectMonitor
Object, change the mark Word lock bit of the lock object header to “10”, and then store the pointer in mark WordObjectMonitor
Object pointer ObjectMonitor
There are two queues in,_WaitSet
and_EntryList
For preservationObjectWaiter
Object list (each thread waiting for a lock is encapsulated asObjectWaiter
Object),_owner
To holdObjectMonitor
Object, which is entered first when multiple threads simultaneously access a piece of synchronized code_EntryList
Collection, when the thread gets the object’smonitor
After entering_Owner
Area and themonitor
In theowner
The variable is set to the same time as the current threadmonitor
Counter incount
+ 1 if called by threadwait()
Method that will release the currently heldmonitor
.owner
Variable restored to NULL,count
Decrement by 1 while the thread entersWaitSet
Waiting to be awakened in the collection. It is also released if the current thread completes executionmonitor
(lock) and reset the value of the variable for other threads to accessmonitor
(lock). As shown in the figure below
The underlying principles of Synchronized code blocks
The Javac compiler uses monitorenter and MonitorerExit to lock and unlock synchronized code blocks. To ensure that the locks are released at the end of a block’s execution or at the end of an exception, the Javac compiler uses monitorerExit at compile time. Monitorerexit performs special processing, as shown in the following example:
public class Hello {
public void test(a) {
synchronized (this) {
System.out.println("test"); }}}Copy the code
See the compiled bytecode in Javap -c:
public class Hello {
public Hello(a);
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public void test(a);
Code:
0: aload_0
1: dup
2: astore_1
3: monitorenter
4: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
7: ldc #3 // String test
9: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;) V
12: aload_1
13: monitorexit
14: goto 22
17: astore_2
18: aload_1
19: monitorexit
20: aload_2
21: athrow
22: return
Exception table:
from to target type
4 14 17 any
17 20 17 any
}
Copy the code
It can be seen from the bytecode that the synchronized statement block is implemented using monitorenter and Monitorexit instructions. Monitorenter refers to the starting position of the synchronized code block, and Monitorexit refers to the ending position of the synchronized code block. When monitorenter is executed, the current thread attempts to retrieve the Monitor stored in the Mark Word. When the monitor’s entry counter is 0, the thread succeeds in acquiring the Monitor and sets the counter value to 1 to retrieve the lock.
If the current thread already owns the Monitor, it can re-enter the monitor and increment the counter by one. If another thread already has ownership of Monitor, the current thread will block until the executing thread completes execution, i.e. the Monitorexit directive is executed. The executing thread will release monitor and set the counter to 0, and the other thread will have the opportunity to own Monitor.
Note that the compiler will ensure that regardless of how the method completes, every Monitorenter directive called in the method executes its monitorexit counterpart, regardless of whether the method terminates normally or abnormally. To ensure that monitorenter and Monitorexit can be paired correctly when the method exception completes, the compiler automatically generates an exception handler that claims to handle all exceptions. The exception handler is intended to execute monitorexit. You can also see from the bytecode above that there are two Monitorexit directives, which are executed to release monitor when the exception ends.
Underlying principles of synchronization methods
The lock and unlock of the synchronization method is realized by Javac compiler, and the underlying is realized by ACC_SYNCHRONIZED access identifier, as shown below:
public class Hello {
public synchronized void test(a) {
System.out.println("test"); }}Copy the code
Method-level synchronization is implicit, that is, controlled without bytecode instructions, and is implemented in method calls and return operations. The JVM can distinguish whether a method is synchronized from the ACC_SYNCHRONIZED access flag in the method_info Structure in the method constant pool. When a method is invoked, the calling instruction checks if the method’s ACC_SYNCHRONIZED access flag is set, and if so, the thread of execution holds Monitor, then executes the method, and finally releases Monitor when the method completes (either normally or abnormally). During method execution, the executing thread holds the Monitor, and no other thread can obtain the same monitor. If an exception is thrown during the execution of a synchronous method and cannot be handled within the method, the monitor held by the synchronous method is automatically released when the exception is thrown outside the synchronous method.
Let’s look at how the bytecode layer is implemented:
public class Hello {
public Hello(a);
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public synchronized void test(a);
Code:
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String test
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;) V
8: return
}
Copy the code
Other optimizations for locks
- Adaptive Spinning: From the lightweight lock acquisition process, we know that the heavy lock is acquired by Spinning when the thread fails to perform a CAS operation on the lightweight lock. The problem is that spin is cpu-consuming, and if the lock is never acquired, the thread will always be spinning, wasting CPU resources. The easiest way to solve this problem is to specify the number of spins, such as loop it 10 times, and then block if the lock has not been acquired. But the JDK takes a smarter approach — adaptive spin, which simply means that a thread spins more next time if it succeeds, and less if it fails.
- Lock Coarsening (Lock Coarsening) : The concept of Lock Coarsening should be easy to understand. It is to merge multiple Lock and unlock operations into one, and expand multiple consecutive locks into a larger range of locks. Here’s an example:
public void lockCoarsening(a) {
int i=0;
synchronized (this){
i=i+1;
}
synchronized (this){
i=i+2; }}Copy the code
The two synchronized code blocks above can become one
public void lockCoarsening(a) {
int i=0;
synchronized (this){
i=i+1;
i=i+2; }}Copy the code
- Lock Elimination: Lock Elimination is code that removes unnecessary Lock operations. In the code below, for example, the following for loop can be removed entirely to reduce the execution of the locking code
public void lockElimination(a) {
int i=0;
synchronized (this) {for(int c=0; c<1000; c++){
System.out.println(c);
}
i=i+1; }}Copy the code