1. An overview of the
Prior to JDK1.6, synchronized was implemented based on the underlying operating system’s Mutex Lock, and each acquisition and release of a Lock resulted in a switch between user mode and kernel mode, thereby increasing the system’s performance overhead. Synchronized locks perform poorly in situations where lock contention is high. JDK 1.6, Java fully optimizes the synchronized synchronization Lock, and in some cases, it outperforms the Lock synchronization Lock
Let’s start with the underlying principles of synchronized, and then use them.
2. Synchronized implementation principle
Synchronized is implemented in the JVM based on entry and exit Monitor objects. But the details of the synchronized method and the synchronized keyword are different. The synchronized code block is implemented using the Monitorenter and monitorexit commands. Method synchronization is implemented implicitly by calling instructions that read the ACC_SYNCHRONIZED flag for methods in the run-time constant pool.
2.1 Java object headers
In the JVM, the in-memory layout of objects is divided into three areas: object headers, instance data, and alignment padding.
- Instance variables: store the property data of the class, including the property information of the parent class and, in the case of the instance part of the array, the length of the array. This part of memory is aligned to 4 bytes.
- Fill data: The VM requires that the start address of the object be an integer multiple of 8 bytes. Populated data is not required to exist, only for byte alignment.
The Java object header is the key to the synchronized implementation, and the locks synchronized uses are in the Java object header.
The lock objects used by synchronized are stored in Java object headers. The JVM uses two word widths (one word for four bytes, one byte for eight bits) to store object headers (if the object is an array, three words are allocated, and the extra word is the array length). Its main structure is composed of Mark Word and Class Metadata Address.
Number of vm bits | Object structure | instructions |
---|---|---|
32/64bit | Mark Word | Stores the object’s hashCode, lock information, or generational age or GC flag |
32/64bit | Class Metadata Address | A type pointer points to the class metadata of an object, and the JVM uses this pointer to determine which class the object is an instance of. |
32/64bit | Array length | The length of the array (if the current object is an array) |
By default, Mark Word stores the object’s HashCode, generational age, lock marker bits, etc. Mark Word stores different contents in different locked states. The default state in 32-bit JVMS is below:
The lock state | 25 bit | 4 bit | 1 bit Indicates whether biased locking is enabled | 2 bit Indicates the lock flag bit |
---|---|---|---|---|
Unlocked state | Object HashCode | Age of object generation | 0 | 01 |
During operation, the data stored in Mark Word changes with the change of the lock flag bit, and there are four possible types of data.
2.2 Low-level implementation of synchronized
As mentioned above, the JVM implements method synchronization and code block synchronization based on entering and exiting Monitor objects.
Monitor is essentially implemented by relying on the underlying operating system’s Mutex Lock. Switching Mutex Locks requires a transition from the user state to the core state, so the state transition takes a lot of processor time. So synchronized is a heavyweight operation in the Java language.
Here’s how synchronized synchronizes blocks of code.
public class SynTest{
public int i;
public void syncTask(a){
synchronized (this){ i++; }}}Copy the code
Decompiled results are as follows:
D:\Desktop>javap SynTest.class
Compiled from "SynTest.java"
public class SynTest {
public int i;
public SynTest();
public void syncTask();
}
D:\Desktop>javap -c SynTest.class
Compiled from "SynTest.java"
public class SynTest {
public int i;
public SynTest();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public void syncTask();
Code:
0: aload_0
1: dup
2: astore_1
3: monitorenter
4: aload_0
5: dup
6: getfield #7 // Field i:I
9: iconst_1
10: iadd
11: putfield #7 // Field i:I
14: aload_1
15: monitorexit
16: goto 24
19: astore_2
20: aload_1
21: monitorexit
22: aload_2
23: athrow
24: return
Exception table:
from to target type
4 16 19 any
19 22 19 any
}
Copy the code
Follow monitorenter and monitorexit:
3: monitorenter
/ / to omit
15: monitorexit
16: goto 24
/ / to omit
21: monitorexit
Copy the code
From the bytecode we know that the implementation of the synchronous block uses the Monitorenter and Monitorexit directives
Let’s look at the synchronization process again:
public class SynTest{
public int i;
public synchronized void syncTask(a){ i++; }}Copy the code
Decompile: javap -verbose -p SynTest
The Classfile/D: / Desktop/SynTest. Class Last modified on April 2, 2020; size 278 bytes SHA-256 checksum 0e7a02cd496bdaaa6865d5c7eb0b9f4bfc08a5922f13a585b5e1f91053bb6572 Compiled from "SynTest.java" public class SynTest minor version: 0 major version: 57 flags: (0x0021) ACC_PUBLIC, ACC_SUPER this_class: #8 // SynTest super_class: #2 // java/lang/Object interfaces: 0, fields: 1, methods: 2, attributes: 1 Constant pool: #1 = Methodref #2.#3 // java/lang/Object."<init>":()V #2 = Class #4 // java/lang/Object #3 = NameAndType #5:#6 // "<init>":()V #4 = Utf8 java/lang/Object #5 = Utf8 <init> #6 = Utf8 ()V #7 = Fieldref #8.#9 // SynTest.i:I #8 = Class #10 // SynTest #9 = NameAndType #11:#12 // i:I #10 = Utf8 SynTest #11 = Utf8 i #12 = Utf8 I #13 = Utf8 Code #14 = Utf8 LineNumberTable #15 = Utf8 syncTask #16 = Utf8 SourceFile #17 = Utf8 SynTest.java { public int i; descriptor: I flags: (0x0001) ACC_PUBLIC public SynTest(); descriptor: ()V flags: (0x0001) ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return LineNumberTable: line 1: 0 public synchronized void syncTask(); descriptor: ()V flags: (0x0021) ACC_PUBLIC, ACC_SYNCHRONIZED Code: stack=3, locals=1, args_size=1 0: aload_0 1: dup 2: getfield #7 // Field i:I 5: iconst_1 6: iadd 7: putfield #7 // Field i:I 10: return LineNumberTable: line 5: 0 line 6: 10 } SourceFile: "SynTest.java"Copy the code
Flags: (0x0021) ACC_PUBLIC, ACC_SYNCHRONIZED
The JVM can tell 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 called, the calling instruction checks to see if the ACC_SYNCHRONIZED access flag is set for the method, and if so, the executing thread will hold the monitor before executing the method, and finally release the monitor when the method completes (either normally or otherwise).
3. Synchronization process (lock upgrade process)
As explained above, synchronized initially relied on operating system mutex, a heavyweight operation. In JDK 1.6, biased locking and lightweight locking were introduced to reduce the performance cost of acquiring and releasing locks. There are four lock states: no lock state, biased lock state, lightweight lock state, and heavyweight lock state. These states are gradually upgraded with the competition, but cannot be degraded. The purpose is to improve the efficiency of lock and lock release.
3.1 biased locking
In most cases, there is no multithreaded contention for locks, and biased locking is designed to improve performance when only one thread executes a synchronized block.
Biased locking the core idea is that if a thread got a lock and then lock into bias mode, the structure of Mark Word become biased locking structure, when the thread lock request again, no need to do any synchronization operation, namely the process of acquiring a lock, which saves a large amount of relevant lock application operation, thus the performance of the provider.
Acquisition process:
- Access whether the bias lock flag in Mark Word is set to 1 and whether the lock flag bit is 01 — confirm the bias state.
- If the state is biased, it tests whether the Thread ID points to the current Thread, and if so, executes the synchronization code.
- If it does not point to the current Thread, the CAS contention Lock is used. If the contention succeeds, the Thread ID in Mark Word is set to the current Thread ID and the current Thread ID is stored in Lock Record in the stack frame.
- If the CAS fails to obtain a biased lock, there is a contention. When the safepoint is reached (at which no bytecode is executing), the biased lock thread is first suspended, and then the biased lock thread is checked to see if it is alive (because the biased lock thread may have finished executing, but it does not actively release the biased lock).
- If the thread is not active, set the object header to unlocked (with the flag bit “01”) and then re-bias the new thread; If the thread is still alive, the biased lock is revoked and the thread is upgraded to the lightweight lock state (with the “00” flag). The lightweight lock is held by the thread that originally held the biased lock and continues to execute its synchronization code, while the competing thread spins and waits to acquire the lightweight lock.
Lock release process:
These are actually four or five steps in the lock acquisition process above.
Biased locking uses a mechanism of waiting until a race occurs to release the lock, so the thread holding the biased lock releases the lock when another thread tries to race the biased lock.
Biased locking is undone by waiting for a global security point (at which point no bytecode is executing).
-
When the global safety point is reached, pause the thread that has the biased lock and check whether it is or not.
-
When the block is inactive or has exited, the object header is set to an unlocked state and then re-biased to the new thread.
-
If it is still alive, it traverses all the Lock records in the thread stack. If it can find the corresponding Lock Record, it indicates that the biased thread is still executing the code in the synchronized block. You need to upgrade to a lightweight Lock and directly modify the Lock Record in favor of the thread stack.
-
The thread that held the biased lock continues to execute its synchronization code, while the competing thread spins and waits to acquire the lightweight lock.
In the Art of Concurrent Programming in Java, this section goes something like this:
When a thread accesses a synchronized block and obtains a lock, it records the thread ID that stores the lock bias in the object header and stack frame. In the future, when the thread enters and exits the synchronized block, it does not need to perform CAS operations to lock and unlock. It only needs to simply test whether the bias lock pointing to the current thread is stored in the Mark Word of the object header. If successful, the thread has acquired the lock. If it fails, it needs to test whether the bias lock identifier in Mark Word is set to 1 (indicating that the current bias lock is). If it is not set, the CAS contention lock will be used. If it is set, the CAS will try to point the bias lock of the object header to the current thread.
Personally, I think this part of the book seems to be a little different, I have checked a lot of blogs, normally according to the logical analysis, should be to determine the lock flag bit, determine the state of the lock, rather than determine whether the lock thread ID is pointing to their own.
The underlying implementation of biased locking, if you want to learn more about it, can refer toThis article – covers the underlying c++ implementation
3.2 Lightweight Lock
Lock acquisition process:
- If the lock object is not biased or is already biased to another thread, then an unlocked state is built
mark word
Set it toLock Record
In the middle, we callLock Record
Store objects inmark word
The field of swat Mark Word is called. - Copy the Mark Word in the copy object header to the lock record. The virtual machine will then use the CAS operation to attempt to update the object’s Mark Word to a pointer to the Lock Record.
- If the update is successful, the current thread acquies the lock and executes the synchronization code. If the update fails, the current thread attempts to spin to acquire the lock.
- When the spin exceeds a certain number of times, or when one thread is holding the lock, one is spinning, and a third is visiting, a lightweight lock expands into a heavyweight lock, which blocks all threads except the one that owns the lock.
Lock release process:
- The CAS operation is used to try to replace the current Mark Word object copied in the thread.
- If the replacement is successful, the synchronization process is complete.
- If the replacement fails, another thread has tried to acquire the lock (which has been inflated) and the suspended thread must be awakened at the same time as the lock is released.
3.3 Heavyweight Locks
For the heavyweight lock, refer to step 4 above. The lightweight lock is expanded to the heavyweight lock, and the lock flag of Mark Word is updated to 10. Mark Word refers to the mutex (the heavyweight lock).
Synchronized’s heavyweight locks are implemented through an internal object called a monitor Lock, which in turn relies on the underlying operating system’s Mutex Lock, as explained at the beginning of this article. The operating system’s ability to switch between threads, which requires a transition from user state to core state, is very costly, and the transition between states takes relatively long, which is why Synchronized is inefficient.
4. Synchronized
Every Java object can be used as a lock in one of the following three ways:
- For instance methods, that is, normal synchronous methods, the lock is the current instance object.
- For statically synchronized methods, the lock is a class object of the current class.
- For a synchronized method block, the lock is an object configured in the synchronized parentheses.
Synchronized refers to two types of locks, one on the object that calls this method, known as an object lock or instance lock, and the other on an object of that class, known as a class lock.
4.1 the object lock
Image understanding:
Every object in Java has a lock, which is unique. Assume that the distribution of an object space, there are multiple methods, the equivalent of space there are multiple small room, if we put all the small room lock, because the object is only one key, so the same time there can only be a person to open a small room, and then run out back, again by the JVM to allocate the next person to get the key.
In this way, for some interview questions are easy to solve.
-
The same object accesses its two synchronized methods in two threads
-
Different objects call the same synchronous method in two threads
The first problem is that, because locks are for objects, when an object calls a synchronized method, other synchronized methods need to wait until their execution is complete and the lock is released.
The second problem is that because there are two objects, the lock is on the object, not the method, so it can be executed concurrently without being mutually exclusive. Figuratively speaking, because each thread is new an object when it calls a method, there will be two Spaces, two keys.
4.2 class lock
There is only one class object, which means that there is only one space at any one time, with N rooms in it, one lock, one key.
Question:
- Use the class to call two different synchronous methods directly in two threads
- Use a static object of a class to call a static or non-static method in two threads
- An object calls one statically synchronized method and one non-statically synchronized method in two threads
Because locking a static object is actually locking a.class, which has only one class object, it can be understood that there is only one space with N rooms and one lock at any one time, so rooms (synchronous methods) must be mutually exclusive. Because it is an object call, both 1 and 2 are mutually exclusive.
The third problem is that, although it is an object call, the two methods have different lock types. The static method that is called is actually called by the class object. That is, the two methods do not produce the same object lock, so they are not mutually exclusive and will be executed concurrently.
Synchronized Optimizes other locks
5.1 lock elimination
Lock removal removes unnecessary lock operations. The VIRTUAL Machine Instant editor removes locks that require synchronization on the code but are detected as unlikely to be competing for shared data at runtime.
5.2 lock coarsening
If a series of consecutive operations lock and unlock the same object repeatedly, even if the locking operation occurs in the body of a loop, then frequent mutex synchronization, even if there is no thread contention, can cause unnecessary performance costs.
If the virtual machine detects a sequence of fragmented operations that lock the same object, it will extend (coarsening) lock synchronization beyond the entire sequence of operations.
5.3 Spin lock and adaptive spin lock
Reasons for the introduction of spin locks:
The biggest performance impact of mutex synchronization is the implementation of blocking, because both suspending and resuming threads need to be transferred to kernel mode, which puts a lot of pressure on the system’s concurrent performance. At the same time, the virtual machine development team also noted that in many applications, the shared data is locked for a short period of time, and it is not worthwhile to frequently block and wake up threads for that short period of time.
The spin lock.
Let the thread execute a meaningless busy loop (spin) and wait for some time without being suspended immediately (spin does not give up processor execution time) to see if the thread holding the lock releases it soon. Spinlocking was introduced in JDK 1.4.2 and is disabled by default, but can be enabled using -xx :+UseSpinning; It is enabled in JDK1.6 by default.
Disadvantages of spin-locks:
Spin-waiting is not a substitute for blocking, and while it avoids the overhead of thread switching, it consumes processor time. If the thread holding the lock releases it very quickly, then spin is very efficient; Spinning threads, on the other hand, drain the processor’s resources by not doing any meaningful work, resulting in wasted performance.
Adaptive spin lock:
JDK1.6 introduces adaptive spin locks. Adaptive means that the number of spins is no longer fixed, it is determined by the previous spin time on the same lock and the state of the lock owner: If a spin wait has just succeeded in obtaining a lock on the same locked object, and the thread holding the lock is running, the virtual machine will assume that the spin is likely to succeed again, and it will allow the spin wait to last a relatively longer time. If the spin is rarely successfully acquired for a lock, it may be possible to omit the spin process in future attempts to acquire the lock to avoid wasting processor resources. In simple terms, if the thread spins successfully, it will spin more times next time, and if it fails, it will spin less.
6. Synchronized reentrant
The mutex is designed to block when a thread attempts to manipulate a critical resource of an object lock held by another thread, but when a thread requests its own critical resource of an object lock again, this is a reentrant and the request succeeds. In Java, synchronized is an atomicity based internal locking mechanism and is reentrant, so that a thread that calls a synchronized method calls another synchronized method inside its method body. This means that it is allowed for a thread to acquire an object lock and then request it again. This is synchronized reentrancy.
7. Summary & References
summary
Synchronized features: ensures memory visibility and atomicity of operations. After optimization by JDK6, synchronized’s performance is actually not as bad as, and sometimes better than, JVM implementation Reentrantlock, which is why many classes in the Java Concurrent package are based on synchronized implementation principles.
The resources
- In-depth understanding of Java synchronization implementation principles
- Summary of Java synchronized principles
- The Art of Concurrent Programming in Java
- Synchronized implementation
- Read the Synchronized low-level implementation to kill your interviewer
- Summary of static and non-static methods