Deep understanding of Synchronized

Synchronized is a keyword provided by the JVM, and Lock is essentially a class. Although both ensure thread synchronization, there are great differences in the specific implementation.

Related to Synchronized is the memory storage layout of objects. (Mark Word does not include the type pointer in the object header) as shown below:

 

  Only the lock status field in the object header is closely related to Synchronized.

Flag bit and meaning of lock

Well, it sounds like you're bored and even want to give up. Let's do something interesting and take you deeper into Synchronized.

  • Import jar package
        <dependency>
            <groupId>org.openjdk.jol</groupId>
            <artifactId>jol-core</artifactId>
            <version>0.9</version>
        </dependency>
  • Writing test classes
package com.zhao.Synchinzed;

import org.openjdk.jol.info.ClassLayout;


public class A {
    public static void main(String[] args) throws InterruptedException {

        A a = new A();
        ClassLayout classLayout = ClassLayout.parseInstance(a);
        synchronized (a){
            System.out.println("At this time, it is unlocked and the console should print '01' ");
        }
        System.out.println(classLayout.toPrintable());

    }
}
  • Console output results

Since the X86 architecture uses small end storage, we need to look backwards if we want to read the information in the object header!

Small end storage is to store the high bit to the high address and the low bit to the low address. Therefore, the corresponding information in the object header should be

  At this time, the lock status is 001 and there is no lock status.

Bias lock

When I add a Synchronized synchronization code block to this class, and use this object as a lock. When there is only one thread, it will enter the biased lock, and the lock state will become 101.

Test class

package com.zhao.Synchinzed;

import org.openjdk.jol.info.ClassLayout;

import java.util.concurrent.TimeUnit;


public class A {
    public static void main(String[] args) throws InterruptedException {

        TimeUnit.SECONDS.sleep(5);  //A delay of at least 4 seconds is required to enter the bias lock
        A a = new A();
        ClassLayout classLayout = ClassLayout.parseInstance(a);
        synchronized (a){
            System.out.println("The bias lock console prints 101 and records thread information");
        }
        System.out.println(classLayout.toPrintable());

    }
}

Console

  Here, someone may ask why there are values on the hash code and some blank filling. I didn't call the hashcode() method to generate the hash code!

Fortunately, you didn't call it. If you call it, you will directly enter the heavyweight lock. What are these values? What is recorded here is the id and timestamp of the thread you obtained the bias lock.

Upgrade from lock free to biased lock process:

The first thread sees that the flag bit of the lock is 01 and the hash code is 0. It is the first thread to obtain the lock. It has the idea of taking it for itself. It records its thread id and timestamp information to the hash code area to complete the lock upgrade.

Lightweight Locking

When a second thread also wants to occupy the lock, the biased lock state will exit, and then upgrade to a lightweight lock. The thread that obtains the lock runs its own code fragment. If it does not obtain the lock, it keeps trying to obtain the lock by spinning.

Test class

package com.zhao.Synchinzed;


import org.openjdk.jol.info.ClassLayout;

import java.util.concurrent.TimeUnit;

public class A {
    public static void main(String[] args) throws InterruptedException {

        TimeUnit.SECONDS.sleep(5);  //A delay of at least 4 seconds is required to enter the bias lock
        A a = new A();
        ClassLayout classLayout = ClassLayout.parseInstance(a);
        synchronized (a){
            System.out.println("The bias lock console prints 101 and records thread information");
        }
        System.out.println(classLayout.toPrintable());

        new Thread((() -> {
            synchronized (a){
                try {
                    TimeUnit.SECONDS.sleep(1);  //You need to sleep for 1 second to make the lock compete. When the second thread sees that the lock is occupied, it will immediately upgrade the lock to a lightweight lock
                    System.out.println("My thread"+Thread.currentThread().getName()+" I'm here to compete for the lock");
                } catch (Exception e) {
                }
            }
        }),"A").start();

        System.out.println(classLayout.toPrintable());

    }
}

Console

  In the unlocked state, each thread stack frame will have a copy of Mark Word called Displaced   Mark   Word. Stored in an area called Lock Record. Each thread wants to write its own Lock Record pointer to the object's Mark Word (this paragraph can't be understood, turn up and down: lock flag bit and meaning). If this thread uses cas successfully, it writes its own Lock Record pointer to the object's Mark Word. At this time, it will change the object's Mark Word lock flag bit to 00, It also stores information such as Mark Word, hashcode and the age of the object in the object header in the heap

The process of upgrading deflection lock to lightweight lock:

The second thread attempts to acquire a lock and writes its Lock Record pointer to make word. Once it obtains the lock, it will write its own Lock Record pointer and modify the flag bit of the lock to 00. There is a CAS process during this process. If the lock flag bit of Mark Word in the heap memory is changed to 00, it means that the CAS has succeeded this time. If the lock flag bit has been changed to 00, it means that the CAS has failed this time.  

Heavyweight lock

If two or more threads compete for a lock, it will expand into a heavyweight lock, and the flag bit of the lock will become   10. In the heap memory object header, Mark Word stores the pointer of the Monitor lock and the flag bit 10 of the lock.

Test class

package com.zhao.Synchinzed;


import org.openjdk.jol.info.ClassLayout;

import java.util.concurrent.TimeUnit;

public class A {
    public static void main(String[] args) throws InterruptedException {

        TimeUnit.SECONDS.sleep(5);  //A delay of at least 4 seconds is required to enter the bias lock
        A a = new A();
        ClassLayout classLayout = ClassLayout.parseInstance(a);
        synchronized (a) {
            System.out.println("The bias lock console prints 101 and records thread information");
        }
        System.out.println(classLayout.toPrintable());

        new Thread((() -> {
            synchronized (a) {
                try {
                    TimeUnit.SECONDS.sleep(1);  //You need to sleep for 1 second to make the lock compete. The second thread sees that the lock is occupied and immediately upgrades the lock to a lightweight lock
                    System.out.println("My thread" + Thread.currentThread().getName() + " I'm here to compete for the lock");
                } catch (Exception e) {
                }
            }
        }), "A").start();

        System.out.println(classLayout.toPrintable());


        new Thread((() -> {
            synchronized (a) {
                System.out.println("My thread" + Thread.currentThread().getName() + " I'm here to compete for the lock");
            }
        }), "B").start();

        System.out.println(classLayout.toPrintable());

    }

}

Console​​​​​​​

  Upgrade from lightweight lock to heavyweight lock:

More and more threads use CAS to try to write their Lock Record pointer to Mark Word. However, one thread has not obtained the lock using CAS. So he did useless work, hoping to get the lock next time. In this way, the CPU will soar quickly because the thread does not get the lock and keeps spinning! Therefore, there is the heavyweight lock below. More than two threads compete for the same lock, and the flag bit of the lock will become "10". What is stored in Mark Word is the monitor pointer and the flag bit of the lock 10.

Synchronization method and bottom implementation of synchronization code block

The code modified by the synchronous code block will add two instructions before and after the bytecode compiled by the compiler, one   monitorenter, a monitorexit, as shown in the following figure:

 0 aload_0
 1 dup
 2 astore_1
 3 monitorenter
 4 getstatic #9 <java/lang/System.out : Ljava/io/PrintStream;>
 7 new #20 <java/lang/StringBuilder>
10 dup
11 invokespecial #21 <java/lang/StringBuilder.<init> : ()V>
14 ldc #22 < my thread >
16 invokevirtual #23 <java/lang/StringBuilder.append : (Ljava/lang/String;)Ljava/lang/StringBuilder;>
19 invokestatic #24 <java/lang/Thread.currentThread : ()Ljava/lang/Thread;>
22 invokevirtual #25 <java/lang/Thread.getName : ()Ljava/lang/String;>
25 invokevirtual #23 <java/lang/StringBuilder.append : (Ljava/lang/String;)Ljava/lang/StringBuilder;>
28 ldc #26 < here to compete for locks >
30 invokevirtual #23 <java/lang/StringBuilder.append : (Ljava/lang/String;)Ljava/lang/StringBuilder;>
33 invokevirtual #27 <java/lang/StringBuilder.toString : ()Ljava/lang/String;>
36 invokevirtual #11 <java/io/PrintStream.println : (Ljava/lang/String;)V>
39 aload_1
40 monitorexit
41 goto 49 (+8)
44 astore_2
45 aload_1
46 monitorexit
47 aload_2
48 athrow
49 return

There are two monitorexits on the top because your code has try...catch... To run the monitorexit on the top normally. If an exception occurs, go to the monitorexit on the bottom.

A thread comes to get the lock. See   The monitorenter instruction will be associated with a monitor object, the monitor record will be incremented by 1, and the monitor pointer will be written to the object header. The flag bit of the modified lock is' 10 ',

This process is reentrant, that is, you call the Synchronized method in the Synchronized method is allowed, you only need to add the monitor counter to 1. When other threads come, they will check that the monitor counter is not 0, they will know that a thread has obtained the lock, and it will wait. When the thread obtaining the lock releases the lock, the monitor counter will be released several times after it is increased several times. Clear the counter to complete the release of the lock! Let other threads continue to compete for the lock.

Synchronization method

public synchronized void SS() {
        System.out.println("Synchronization method");
    }

public synchronized void SS();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #7 / / String synchronization method
         5: invokevirtual #6                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 14: 0
        line 15: 8
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       9     0  this   Lcom/zhao/Synchinzed/B;
}

When the JVM detects' ACC '_ Synchronized 'will be called implicitly after this flag bit   Monitorenter, and monitorexit. Add monitorenter before calling the method. After executing the synchronization method, monitorexit will be called.

Volatile keyword and thread

If you want to make Volatile clear, you must first talk about the computer model. However, only three things are involved here: CPU, main memory and hard disk. Their three speeds, CPU is like an airplane, and main memory is like a car. Hard drives are like bicycles. In the computer, the interaction between the CPU and the main memory is common, but their speed difference is too much. It takes 100s to read data from the memory, and the CPU finishes processing in one second. In the remaining 99 seconds, the CPU is idle there.

Based on the principle that idleness is waste, a lot of cache is added at the CPU level to solve the problem of slow reading. When data is read from memory for the first time, the data is placed in the cache. The next time you read the same data, you don't need to read it from memory.

Our threads are similar. They read data from the main memory to their own working memory. When they read the data in the working memory directly the next time, they don't need to interact with the heap memory frequently.

But it's easy

Keywords: Java Back-end

Added by websitesca on Wed, 08 Dec 2021 21:51:36 +0200