The opening shoot the breeze

Migrant workers, migrant soul, we were born with the best. When “capitalism” gradually shackles us (big) on (leek) people (food) body, at that moment I understand that the sun never sets empire · capitalist harvester · Lucky lucky coffee is how great, although I do not understand coffee, but still want to say thank you! Speaking of coffee, wanting to go to the bathroom after drinking is not very friendly to bug writer, since I don’t (very) like being paid to go to the bathroom.

Back to this lame Java article, the main news is… Tui ~~ mouth ladle. As mentioned at the end of the last article, I will mainly comb my understanding of Synchronized in an all-round way. If I can help you to defeat the interviewer, IT will be a great honor.

Synchronized origin

It was a dark night and Doug Lee was working late at night drinking coffee and holding his urine just like us, but he was writing JDK and we were writing bugs with his JDK. Before creating JDK1.5, he forgot to provide an extensible synchronization interface or method in the Java language, so he gave us a bad Synchronized interface before JDK1.5, and then added the Lock interface and a lot of native and distributed packages for us to use. As a result, Synchronized has been around for a long time as a form of keyword, and many improvements have been made to it in subsequent JDK1.6 releases to improve its performance and make it competitive with Lock. Ok, that’s all. Bye!

What is Synchronized

If I were to say that Synchronized is an implicit non-fair reentrance heavyweight lock based on an object monitor in the JVM, it is automatically unlocked within the JVM, blah blah blah… “Interview essay” for short, obviously I can’t write this, it would be faster to just post a link to a blog. Implicit locking is implemented based on the operating system’s MutexLock. Each unlock operation causes a switch between user mode and kernel mode, resulting in a lot of extra overhead. Baidu can learn about the user state and the definition of the kernel state, here is not redundant. At the same time, the process of adding and unlocking Synchronized is uncontrollable for developers and loses scalability.

Here’s an example of what Synchronized looks like when compiled:

/** * FileName: SynchronizeDetail * Author: RollerRunning * Date: 2020/11/30 10:10 PM * Description: Synchronized */ SynchronizeDetail {public Synchronized void testRoller() {system.out.println ("Roller "); Synchronized */ SynchronizeDetail {public Synchronized void testRoller() {system.out.println ("Roller ") Running!" ); } public void testRunning(){ synchronized (SynchronizeDetail.class){ System.out.println("Roller Running!" ); }}}Copy the code

Compile the above source code and print the compiled code:

public com.design.model.singleton.SynchronizeDetail(); descriptor: ()V flags: 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 9: 0 public synchronized void testRoller(); descriptor: ()V flags: ACC_PUBLIC, ACC_SYNCHRONIZED Code: stack=2, locals=1, args_size=1 0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; 3: ldc #3 // String Roller Running! 5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;) V 8: return LineNumberTable: line 14: 0 line 15: 8 public void testRunning(); descriptor: ()V flags: ACC_PUBLIC Code: stack=2, locals=3, args_size=1 0: ldc #5 // class com/design/model/singleton/SynchronizeDetail 2: dup 3: astore_1 4: monitorenter 5: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; 8: ldc #3 // String Roller Running! 10: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;) V 13: aload_1 14: monitorexit 15: goto 23 18: astore_2 19: aload_1 20: monitorexit 21: aload_2 22: athrow 23: return Exception table: from to target type 5 15 18 any 18 21 18 any LineNumberTable: line 17: 0 line 18: 5 line 19: 13 line 20: 23 StackMapTable: number_of_entries = 2 frame_type = 255 /* full_frame */ offset_delta = 18 locals = [ class com/design/model/singleton/SynchronizeDetail, class java/lang/Object ] stack = [ class java/lang/Throwable ] frame_type = 250 /* chop */ offset_delta = 4 }Copy the code

Looking at the compiled code, there is a line in the testRoller() method that describes flags: ACC_PUBLIC, ACC_SYNCHRONIZED, indicates that the current method access is SYNCHRONIZED state, and this flag is added by the JVM after compilation according to the location of SYNCHRONIZED lock, also known as the class lock, any thread that executes the method, Both threads need to acquire the Monitor object before allowing other threads to hold the Monitor object until the lock is released. Take HotSport vm as an example. The bottom layer of Monitor is ObjectMonitor, which is implemented based on C++. I do not understand C++.

ObjectMonitor::ObjectMonitor() { _header = NULL; _count = 0; _waiters = 0, _recursions = 0; // thread reentrant count _object = NULL; _owner = NULL; // Identifies the thread that owns the monitor _WaitSet = NULL; _WaitSetLock = 0; _Responsible = NULL ; _succ = NULL ; _cxq = NULL ; FreeNext = NULL; FreeNext = NULL; _EntryList = NULL ; // The queue of threads waiting for the lock block is also a bidirectional list. _SpinClock = 0 ; OwnerIsThread = 0 ; }Copy the code

So let’s use a diagram to illustrate the process of acquiring the testRoller() lock in a multi-threaded concurrency situation

MutexLock has been mentioned above, and in the figure, unlocking and acquiring Monitor objects are mutually exclusive operations based onit. Again, during the process of adding and unlocking, threads will switch between kernel state and user state, thus sacrificing some performance.

For the testRunning again () method, obviously, in the compiled class appeared a monitorenter/monitorexit, actually is another form of monitor object, is essentially the same, but the difference is that objects when lock instance method or instance object called a built-in lock. TestRoller () is the permission control for the class (the object’s class), and the two do not affect each other.

To explain the basic concepts of Synchronized, here’s how it relates to the memory layout of objects in space.

Synchronized and object heap space layout

Again, using the HotSport version of the JVM on a 64-bit operating system as an example, let’s take a look at a graph that has been searched all over the web

This figure shows the information that MarkWord’s 64 bits record in various lock states, including the HashCode of the object, the thread ID of the partial lock, the GC age, and the pointer to the lock. Remember the location of the GC flag record here, and future JVM articles will also use it. To modify the memory layout slightly from the previous example, the code looks like this:

/** * FileName: JavaObjectMode * Author: RollerRunning * Date: 2020/12/01 20:12pm */ public class JavaObjectMode {public static void main(String[] args) {// create object Student Student = new Student(); Synchronized (student) {/ / get the object layout content after locking the String s = ClassLayout. ParseInstance (student). ToPrintable (); // Print the object layout system.out.println (s); } } } class Student{ private String name; private String address; public String getName() { return name; } public void setName(String name) { this.name = name; } public String getAddress() { return address; } public void setAddress(String address) { this.address = address; }}Copy the code

The first graph is the memory layout of the object without the lock. The second graph is the memory layout of the object with the lock. Look at the VALUE of VALUE

After the lock is added, the VALUE of the MarkWord in the object header is changed to record the current lock status. From the VALUE of the first line of the VALUE in the first picture you can see the current lock is marked 001 (which involves a large end of the order and small order problem, can learn: blog.csdn.net/limingliang…). , the corresponding table happens to be unlocked, and the actual code is also unlocked. As shown in Figure 2, the current lock is marked 000 (note: the same position as 001), and Synchronized is a lightweight lock. Because locks have been optimized since JDK1.6, Synchronized will slowly upgrade to heavyweight mutex as the competition heats up.

But there is a problem, why the lock, is lightweight and not biased locking, lock the reason is that in the initialization locked tag in the JVM default delay 4 s create biased locking, by – XX: BiaseedLockingStartupDelay = XXX control. Once a bias lock is created, it is called an anonymous bias lock when no thread is using the current bias lock. That is, the bias thread ID in the table above is null. When a thread comes to add the lock, it evolves into a bias lock.

The lock is just a bunch of flag bits, so let me write some if-else

Synchronized Lock upgrade process

The lock upgrade process is: biased lock -> biased lock -> Lightweight lock -> Heavyweight lock. This process varies gradually with the intensity of thread contention.

Biased locking

The anonymous bias lock has been mentioned before. The function of bias lock is that when the same thread access the synchronized code for many times, the thread only needs to obtain whether it is biased lock in MarkWord, and then determine whether the biased thread ID is its own, that is, two if-else. If it is found that the biased thread ID is its own thread ID, it will execute the code. Otherwise, it will try to obtain the lock through CAS. Once CAS fails to obtain the lock, it will perform the biased lock cancellation operation. This process can be costly in high concurrency scenarios, so use biased locking with caution. The picture shows the memory layout of biased locks

Lightweight lock

Lightweight locking is a CAS-based operation and is suitable for scenarios where competition is not very intense. Lightweight locks are divided into spin locks and adaptive spin locks. Spin-lock: Light weight lock is implemented based on CAS theory, so when resources are occupied and other threads fail to grab the lock, they will be suspended and enter the blocking state. When the resources are ready, they will be woken up again. Such frequent blocking to wake up applied resources is very inefficient, resulting in spin-lock. In JDK1.6, the JVM can set the -xx :+UseSpinning parameter to enable spin locking, and the -xx :PreBlockSpin parameter to set the number of spin locks. However, in JDK1.7 and later, when the spin lock parameter is removed, the JVM no longer supports user-configurable spin locks, hence the emergence of adaptive spin locks. Adaptive spin locks: The JVM dynamically decides to obtain the number of spins of a thread that failed to lock based on how long the previous thread held the spin lock and the state of the lock owner to optimize the resource usage of threads with a large number of CAS states due to overthreading spin. The following figure shows the lightweight memory lock layout:

As the number of threads increased and the competition became more intense, CAS waits were no longer sufficient, so lightweight locks moved to heavyweight locks. The key condition for upgrading prior to JDK1.6 was the number of spin waits exceeded. After JDK1.7, due to uncontrolled parameters, the JVM decides when to upgrade. Several important factors include how long a single thread holds a lock, how long a thread switches between user and kernel states, how long a thread hangs and blocks, how long it wakes up, and how long it takes to reapply for resources

Heavyweight lock

When upgrading to a heavyweight lock, however, there is nothing to be said. The lock bit is 10, and all threads queue up to execute the code with the 10 flag. Each of the locks mentioned above and the lock upgrade process are actually accompanied by a change in the lock bit in MarkWord. I believe that you understand that the lock of different periods corresponds to the different flag information of the head of the object in the heap space. Heavyweight lock’s memory layout I simulated along while also did not give the effect, interested big guy can talk about.

Finally, attach a picture to show the upgrading process of the lock. Drawing is not easy. Please pay attention to it:

Lock the optimization

1. Dynamic compilation to achieve lock elimination

At compile time, the compiler performs escape analysis on the locked code to determine whether the currently synchronized code can only be accessed by one thread and has not been released to other threads (other threads have no access). When confirmed, the bytecode corresponding to the Synchronized keyword is discarded in the compiler.

2. Lock coarsening

In the compilation stage, if the compiler detects that two adjacent code blocks use the Synchronized keyword, the two code blocks are combined to reduce the performance loss caused by the same thread in the process of entering and entering two Synchronized code blocks.

3. Reduce the lock granularity

This is something that needs to be done at the development level. The scope of the lock should be as clear as possible and the scope should be reduced. Best practice: Segmented locking in 1.7 and earlier ConcurrentHashMap. But not anymore.

Finally, thank you audience master, also please three!!