Well, today we talk about synchronized. At first glance, you might think that synchronized is too stupid, too heavy, and stupid to use. Because jdK1.6 after the optimization of it, it is not you used to know it, There is an old Chinese saying, called: don’t three days when shaving each other, today we will take a look at this lock in the end what optimization. I hope those of you who see this video give it a thumbs up.
Synchronized can be used in the following three ways
- For normal synchronization methods, the lock is the current instance object
- Statically synchronized methods where the lock is the class object of the current class
- Synchronized method block, lock is the object inside parentheses
When a thread accesses a block of synchronized code, it needs to acquire the lock to execute and release the lock when it exits or throws an exception. So how does that work? Let’s look at a piece of code, okay
public class SynchronizedTest {
public synchronized void test1(){
}
public void test2(){
synchronized (this){
}
}
}
Copy the code
Let’s use Javap to analyze the compiled class file and see how synchronized is implemented
As you can see from this screenshot, the synchronization block is implemented using the Monitorenter and Monitorexit directives,
Monitorenter inserts at the beginning of the block, Monitorexit inserts at the end of the block, and when the Monitor is held, it is locked, that is, locked.
Let’s take a closer look at two important concepts used by Synchronized to implement locking: Java object headers and Monitor
Java object header:
Synchronized; a synchronized lock is stored in an object header consisting of two parts: a Mark Word; a Klass Pointer
Mark Word stores the object’s own runtime data, such as HashCode, GC generation age, lock status flags, thread-held locks, biased lock IDS, and so on. Klass Pointer is a Pointer to a Java object’s class metadata that the JVM uses to determine which class the object is an instance of
Monitor:
Every Java object comes out of the womb with an invisible lock called an internal lock or monitor lock, which we can think of as a synchronization mechanism,
It’s a thread-private data structure,
The structure of monitor is as follows:
- Owner: The initial value is NULL, indicating that no thread owns the Monitor Record. When the thread successfully owns the lock, the unique identifier of the thread is saved. When the lock is released, it is set to NULL.
- EntryQ: Associated with a system mutex (Semaphore) that blocks all threads that attempt to lock a Monitor Record that fails.
- RcThis: Represents the number of all threads that are blocked or waiting on the Monitor Record.
- Nest: Used to implement the count of reentrant locks.
- HashCode: Holds the HashCode value (and possibly GC age) copied from the object header.
- Candidate: used to avoid unnecessary obstruction or waiting thread to wake up, because each time only one thread can have lock, if every time a lock is released before the thread wakes up all threads that are being blocked or wait for, will cause unnecessary context switch (from blocking to ready then lock failure because competition is blocked) resulting in serious decline in performance. Candidate has only two possible values: 0 means there are no threads to wake up and 1 means a successor thread to wake up to compete for the lock.
Lock optimization:
JDK1.6 introduces a number of optimizations such as spin locking, adaptive spin locking, lock elimination, lock coarser, biased locking, and lightweight locking. Locks mainly exist in four states, the order is: no lock state, biased lock state, lightweight lock state, heavyweight lock state, they will gradually upgrade with the fierce competition. One thing, however, is that you can’t degrade the lock
One, spin lock:
Frequent blocking and waking up of threads is a heavy load on the CPU and can put a lot of stress on the system. At the same time, many lock states only last for a short period of time, and it is not worth it to frequently block and wake up threads for this short period of time. So spin locks. The so-called spin lock is to let the thread wait a period of time, not immediately suspended, to see if the thread holding the lock will soon release the lock, if released, can grab the lock. So how can we wait? In fact, it is executing a meaningless loop, and you suddenly feel low, it is executing a for loop, don’t jump to conclusions, let’s continue to analyze
Execute a meaningless loop. If the thread holding the lock releases it quickly, the spin is very efficient. But if spin doesn’t grab the lock for a long time, then spin is a waste of resources, or worse, a dog in the manger. Therefore, there must be a limit to the amount of time or number of spins to wait, and if the lock is not acquired after the defined time, it is suspended.
Spin-locking was introduced in JDK 1.4.2 and is disabled by default, but can be enabled using -xx :+UseSpinning and is enabled by default in JDK1.6. The default number of simultaneous spins is 10, which can be adjusted with -xx :PreBlockSpin; But no matter how you adjust these parameters, you can’t satisfy the unpredictable. So JDK1.6 introduced adaptive spin locks to make virtual machines smarter and smarter.
Two, adapt to the spin lock
JDK 1.6 introduced a more clever spin lock called adaptive spin locking. The number of spins will change, and to put it in more general terms, the thread will spin more if it succeeded last time, because the virtual machine thinks that if it succeeded last time, it will probably succeed again. Conversely, if a lock rarely spins successfully, subsequent spins are reduced or even omitted to avoid wasting processor resources. You don’t feel so low now
Three, lock elimination
In plain English, locking is when you use a lock in a program, but the JVM detects that there is no shared data contention in the program, meaning that the variable has not escaped the method
We programmers write code knowing where locks are needed and where they are not, but sometimes we accidentally use thread-safe apis such as StringBuffer, Vector, HashTable, etc., without showing that locks are used. Like the next piece of code
public void sbTest(){
StringBuffer sb= new StringBuffer();
for(int i = 0 ; i < 10 ; i++){
sb.append(i);
}
System.out.println(sb.toString());
}
Copy the code
In the above code, the JVM can obviously detect that sb has not escaped from sbTest(), so the JVM can boldly eliminate the lock operation inside sbTest.
Four, lock coarsening
As we all know, when using a lock, we should keep the scope of the lock as small as possible, in order to execute as little code as possible in the lock, shorten the time of holding the lock, and other threads waiting for the lock can get the lock as soon as possible. This is the right thing to do in most cases. However, continuous locking and unlocking operations can cause unnecessary performance losses, such as the following for loop:
Lock before coarsening: for (...) {synchronized (obj) {// Synchronized (this) {for (...) {// Some operations}}Copy the code
You get a sense of what lock coarsening means. It is to connect a number of continuous locking and unlocking operations together to expand into a larger range of locks. That is, lock unlocking moves out of the for loop.
Five, biased lock
When we create an object, part of the Markword key data for that object is as follows.
bit fields | Biased lock | Lock flag bit |
---|---|---|
hash | 0 | 01 |
As can be seen from the figure, the flag bit of biased lock is “01” and the status is “0”, indicating that the object has not been biased lock. (” 1 “means biased lock). The moment the object is created, there is a biased lock flag, which also indicates that all objects are biased, but all objects are in a state of “0”, which also indicates that the biased lock of all created objects is not in effect.
However, when the thread reaches the critical section, it inserts the thread ID into Markword using CAS(Compare and Swap) and changes the flag bit biased towards the lock.
The so-called critical area is the area where only one thread is allowed to perform operations, that is, to synchronize code blocks. CAS is an atomic operation
The Mark word structure is as follows:
bit fields | Biased lock | Lock flag bit | |
---|---|---|---|
threadId | epoch | 1 | 01 |
At this point, the biased lock status is “1”, indicating that the biased lock of the object is effective, and it can also be seen which thread has acquired the lock of the object.
Biased locking is a lock optimization introduced in JDK1.6, where “biased” is the bias of an eccentric. This means that the lock is biased in favor of the first thread to acquire it, and if the lock is not acquired by another thread during subsequent execution, and no other thread is competing for the lock, the thread holding the biased lock will never need to synchronize. In other words, if the thread enters or exits the same block of synchronized code again, it does not need to lock or unlock it. Instead, it does the following steps:
- Load-and-test: simply check whether the current thread ID matches the thread ID in Markword.
- If so, the thread has successfully acquired the lock and proceeds with the following code.
- If not, check to see if the object is still biased, that is, the value of the biased lock flag bit.
- If not already biased, the CAS operation is used to compete for the lock, that is, when the lock is first acquired.
The release of bias lock uses a mechanism that only competition can release the lock. Threads do not actively release bias lock and need to wait for other threads to compete. Revocation of bias locks requires waiting for the global safe point (the point in time when no code is executing). The steps are as follows:
- Suspend the thread with bias lock to judge whether the lock object stone is still locked;
- Undo biased lock and return to lockless state or lightweight lock state;
The stop the word (STW) function deteriorates performance. In this case, disable STW.
View the pause – safe point pause log
To view the safe point pauses, you can turn on the safe point log by setting the JVM parameter –
XX: + PrintGCApplicationStoppedTime will play system to stop time,
Add – XX: + PrintSafepointStatistics – XX: PrintSafepointStatisticsCount = 1 these two parameters will print out the detailed information, you can look at the use of biased locking to pause, time is very short, but the contention for serious cases, There are a lot of pauses;
Note: security point logs cannot always be opened: 1. Security point logs are output to stdout by default, because of the cleanliness of stdout logs, and because files redirected by stdout may be locked if they are not in /dev/shm. 2. For some very short pauses, such as unbiased locking, printing costs more than the pauses themselves. 3. Security point logs are printed at security points, which increases the pause time of security points.
So security logs should only be opened during troubleshooting. If to open on the production system, and then add the following four parameters: – XX: XX: + UnlockDiagnosticVMOptions – -displayvmOutput-xx :+ logvmoutput-xx :LogFile=/dev/ SHM /vm.log Open the Diagnostic (only more flags are open for optional, no flag is actively activated), and disable the output of VM logs to stdout. Output to a separate file, the /dev/shm directory (in-memory file system).
The log is divided into three parts: the first part is the timestamp, the type of VM Operation, and the second part is the thread overview, enclosed in brackets. Total: the total number of threads in the safe point initially_RUNNING: Wait_to_block: The number of threads that need to wait for the VM Operation to pause before it starts
The third part is about the stages at the safe point and the time it takes to perform the operation, the most important of which is the VMOP
Spin: time to wait for thread to respond to SafePoint call; Block: the time it takes to suspend all threads; Sync: equal to spin+block, which is the time from the start to the safe point, which can be used to judge the time to enter the safe point; Cleanup: time spent cleaning up; Vmop: The time when the VM Operation is actually executed. As you can see, there are many but short security points, all of which are RevokeBias. High-concurrency applications will disable bias locking.
The JVM turns bias locking on/off
Open the biased locking: – XX: XX: + UseBiasedLocking BiasedLockingStartupDelay = 0 closed biased locking: – XX: – UseBiasedLocking
6. Lightweight locks
The goal of spin locking is to reduce the cost of thread switching. If locks are hotly contested, we have to rely on heavyweight locks to block the threads that failed to compete. If there is no actual lock competition at all, then applying for heavyweight locks is a waste. The goal of lightweight locks is to reduce the performance cost of using heavyweight locks without actual contention, including kernel-user switching due to system calls, thread switching due to thread blocking, and so on.
As the name suggests, lightweight locks are relative to heavyweight locks. With lightweight locks, there is no need to apply for mutex and only point partial byte CAS updates in Mark Word to Lock Record in thread stack (Lock Record: LOCKRECOD tablespace is created in the stack frame of the current thread to copy the data in Mark Word. If the update is successful, the lightweight lock is successfully obtained and the lock status is recorded as lightweight lock. Otherwise, it means that a thread has already acquired the lightweight lock, there is a lock contention (no longer suitable for using the lightweight lock), and then it expands to the heavyweight lock.
Of course, since lightweight locks are naturally aimed at scenarios where lock competition does not exist, if there is lock competition but it is not fierce, it can still be optimized with spin locks, which fail to expand to heavyweight locks.
Cons: Similar to spin locks: if locks are competitive, lightweight locks quickly swell to heavyweight locks, and maintaining lightweight locks becomes wasteful.
7. Heavyweight lock
Once a lightweight lock expands, it is upgraded to a heavyweight lock. Heavyweight locks are implemented by the internal monitor lock, which in turn relies on the operating system MutexLock, so heavyweight locks are also called mutexlocks. When a lightweight lock is upgraded to a heavyweight lock through steps such as lock cancellation, its Markword data is roughly as follows
bit fields | Lock flag bit |
---|---|
A pointer to Mutex | 10 |
Why are heavyweight locks expensive
When the system detects that the lock is a heavyweight lock, it blocks the thread waiting to acquire the lock. The blocked thread does not consume the CPU. But blocking or waking up a thread requires the operating system to help, and that requires switching from user to kernel, which takes a lot of time, perhaps longer than the user executes the code. This is why heavyweight threads are expensive.
Mutex (heavyweight) locks are also known as blocking synchronous, pessimistic locks
Eight, summary
Biased locks, lightweight locks are optimistic locks, heavyweight locks are pessimistic locks. When an object is first instantiated and no threads are accessing it. It’s biased, meaning it now thinks that only one thread can access it, so when the first thread accesses it, it favors that thread, and in that case, the object holds the biased lock. Bias to the first thread. This thread uses CAS when it changes the object header to bias lock and changes the ThreadID in the object header to its own ID. When it accesses the object again, it only needs to compare the IDS and does not need to use CAS for operations. Once a second thread to access the object because the biased locking does not take the initiative to release, so the second thread can see objects to state, at this time that already there is competition on the object, check whether the original owners of the thread lock the object is still alive, if you hang up and the object can be become unlocked state, then back to the new thread, If the original thread is still alive, the stack of that thread is immediately executed to check the usage of the object, and if bias locks are still needed, bias locks are upgraded to lightweight locks (this is when bias locks are upgraded to lightweight locks). If no use exists, you can revert the object to an unlocked state and then re-bias it. Lightweight locks consider contention to exist, but the degree of contention is minimal. Usually two threads will stagger operations on the same lock, or wait a little (spin) before the other thread releases the lock. But when the spin exceeds a certain number, or when one thread is holding the lock, one is spinning, and a third person is calling, the lightweight lock bulges into a heavyweight lock, which blocks all threads except the one that owns the lock, preventing the CPU from idling.
If you have any IT questions, you can send me a private message in the wechat public account and I will answer IT. You can follow IT by scanning the code