Synchronized usage principle and lock optimization upgrade process (interview)

table of Contents

 

Introduction

Synchronized use level

synchronized JVM level

Synchronized optimization level

to sum up


Introduction


Multithreading has always been the focus and difficulty in interviews. No matter what level you are in, learning the synchronized keyword is unavoidable. This is my experience. Let's make a systematic description of synchronized with the thinking of an interview. If an interviewer asks you, tell me about your understanding of synchronized? You can answer systematically from three aspects: the synchronized use level , the synchronized JVM level , and the synchronized optimization level . Maybe the interviewer will look at you with admiration! The article will have a lot of code that is easy to understand. If you have time, you must do it yourself to deepen your understanding and memory. If this article can be helpful to you, I am most gratified on the road of creation.

image.png

Synchronized use level


Everyone knows that synchronized is a lock, what exactly is a lock ? For example, you can understand the lock as the only key to the lock on the toilet door. Everyone can only use this key to open the toilet door when going in. This key can only be owned by one person at a time The person with the key can enter and exit the toilet repeatedly. In the program, we call this repeated entry and exit behavior of the toilet called the reentrant lock. It can modify static methods, instance methods and code blocks, so let's take a look at the meaning of synchronized used to synchronize code locks.

  • For ordinary synchronization methods, the object instance is locked.
  • For static synchronization methods, it is the Class object of the class that is locked.
  • For synchronized code blocks, the objects in parentheses are locked.

Let me talk about the concepts of synchronization and asynchrony.

  • Synchronization: alternate execution.
  • Asynchronous: Simultaneous execution.

For example, for example, eating and watching TV are two things. You can watch TV after eating. The two things are sequential in the time dimension, which is called synchronization. You can eat and watch TV shows at the same time. In the time dimension, you can proceed at the same time in no particular order. After the meal is finished, you can watch TV and you can learn. This is asynchrony. The advantage of asynchrony is that it can improve efficiency. You can save time to learn.

Let's take a look at the code. There are detailed comments in the code, which can be copied to the local for testing. If you have synchronized-based children's shoes, you can skip the explanation on the use of locks.

/**
 * @author :jiaolian
 * @date :Created in 2020-12-17 14:48
 * @description:测试静态方法同步和普通方法同步是不同的锁,包括synchronized修饰的静态代码块用法;
 * @modified By:
 * 公众号:叫练
 */
public class SyncTest {

    public static void main(String[] args) {
        Service service = new Service();
        /**
         * 启动下面4个线程,分别测试m1-m4方法。
         */
        Thread threadA = new Thread(() -> Service.m1());
        Thread threadB = new Thread(() -> Service.m2());
        Thread threadC = new Thread(() -> service.m3());
        Thread threadD = new Thread(() -> service.m4());
        threadA.start();
        threadB.start();
        threadC.start();
        threadD.start();

    }

    /**
     * 此案例说明了synchronized修饰的静态方法和普通方法获取的不是同一把锁,因为他们是异步的,相当于是同步执行;
     */
    private static class Service {
        /**
         * m1方法synchronized修饰静态方法,锁表示锁定的是Service.class
         */
        public synchronized static void m1() {
            System.out.println("m1 getlock");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("m1 releaselock");
        }

        /**
         * m2方法synchronized修饰静态方法,锁表示锁定的是Service.class
         * 当线程AB同时启动,m1和m2方法是同步的。可以证明m1和m2是同一把锁。
         */
        public synchronized static void m2() {
            System.out.println("m2 getlock");
            System.out.println("m2 releaselock");
        }

        /**
         * m3方法synchronized修饰的普通方法,锁表示锁定的是Service service = new Service();中的service对象;
         */
        public synchronized void m3() {
            System.out.println("m3 getlock");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("m3 releaselock");
        }

        /**
         * 1.m4方法synchronized修饰的同步代码块,锁表示锁定的是当前对象实例,也就是Service service = new Service();中的service对象;和m3一样,是同一把锁;
         * 2.当线程CD同时启动,m3和m4方法是同步的。可以证明m3和m4是同一把锁。
         * 3.synchronized也可以修饰其他对象,比如synchronized (Service.class),此时m4,m1,m2方法是同步的,启动线程ABD可以证明。
         */
        public void m4() {
            synchronized (this) {
                System.out.println("m4 getlock");
                System.out.println("m4 releaselock");
            }
        }

    }
}

After the above test, you may have questions. Since the lock exists, where is it stored? Answer: Inside the object. Let's use the code to prove it.

Locked in the object header, an object includes the object header, instance data and alignment padding. The object header includes MarkWord and the object pointer. The object pointer points to the object type in the method area. The instance object is attribute data. An object may have many attributes, and the attributes are dynamic. Alignment padding is to make up the number of bytes. If the size of the object is not an integral multiple of 8 bytes, you need to make up the remaining number of bytes, which is convenient for the computer to calculate. In a 64-bit machine, the object header of an object generally occupies 12 of its own size, and in a 64-bit operating system generally occupies 4 bytes, so MarkWord is 8 bytes.

MarkWord includes object hashcode, bias lock flag, thread id and lock identification. In order to facilitate the test of the content of the object header, the dependency package of maven openjdk needs to be introduced.

<dependency>
  <groupId>org.openjdk.jol</groupId>
  <artifactId>jol-core</artifactId>
  <version>0.10</version>
</dependency>
/**
 * @author :duyang
 * @date :Created in 2020-05-14 20:21
 * @description:对象占用内存
 * @modified By:
 *
 *  Fruit对象头是12字节(markword+class)
 *  int 占4个字节
 *
 *  32位机器可能占8个字节;
 *
 *  Object对象头12 对齐填充4 一共是16
 */
public class ObjectMemory {
    public static void main(String[] args) {
        //System.out.print(ClassLayout.parseClass(Fruit.class).toPrintable());
        System.out.print(ClassLayout.parseInstance(Fruit.class).toPrintable());
    }
}

/**
 *Fruit 测试类
 */
public class Fruit {

    //占一个字节大小
    private boolean flag;

}

 

Test result : The 3 rows with red lines below represent the object header, instance data and alignment padding respectively. The object header is 12 bytes, a boolean field flag of the instance data Fruit object occupies 1 byte in size, and the remaining 3 bytes are the alignment padding part, which is 16 bytes in total.

image.png

 

Huh? What about the lock you said, why didn't you see it? Boy, don't worry, let's analyze it in detail later when we talk about the level of synchronized upgrade and optimization. Let's first analyze the meaning of synchronized at the JVM level.

image.png

 

Finally, the above graphic summary:

image.png

 

synchronized JVM level


/**
 * @author :jiaolian
 * @date :Created in 2020-12-20 13:43
 * @description:锁的jvm层面使用
 * @modified By:
 * 公众号:叫练
 */
public class SyncJvmTest {
    public static void main(String[] args) {
        synchronized (SyncJvmTest.class) {
            System.out.println("jvm同步测试");
        }
    }
}

In the above case, we simply output a sentence in the synchronization code block. We mainly look at how it is implemented in jvm. We use Javap -v SyncJvmTest.class to decompile the above code, as shown in the figure below.

image.png

The first line of the above figure has a monitorenter and the sixth line a monitorexit. The jvm instruction in the middle (line 2-5) corresponds to the code of the main method in the Java code. Synchronized depends on the implementation of these two instructions. Let's take a look at the monitorenter semantics in the JVM specification .

  1. Each object has a lock. When a thread enters the synchronization code block, it will acquire the monitor object lock held by this object (implemented in C++). If the current thread acquires the lock, the number of monitor object entries will be incremented by one.
  2. If the thread enters repeatedly, it will increment the entry number of the monitor object once again.
  3. When other threads enter, other threads will be queued in the waiting queue until the thread acquiring the lock sets the entry number of the monitor object to 0 to release the lock, and then other threads have the opportunity to acquire the lock.

 

Synchronized optimization level


Synchronized is a heavyweight lock, mainly because thread competition locks will cause the operating system to switch between user mode and kernel mode, and waste resources. The efficiency is not high. Before jdk1.5, synchronized did not make any optimization, but it did performance in jdk1.6. Optimization, it will go through the process of biased locks, lightweight locks, and finally heavyweight locks. It has a great improvement in performance. ConcurrentHashMap in jdk1.7 is based on ReentrantLock to achieve locks, but in jdk1. After 8, it was replaced with synchronized. From this we can see that the JVM team is quite confident in the performance of synchronized. Below we will introduce lock-free, biased lock, lightweight lock, and heavyweight lock respectively. Below we draw a picture to describe the storage status of these levels of locks in the object header. as the picture shows.

image.png

  • no lock. If you do not add the synchronized keyword, it means no lock, which is easy to understand.
  • Bias lock.
    • Upgrade process: When the thread enters the synchronization block, Markword will store the id of the biased thread and cas will mark the Markword lock state as 01. Whether bias is used to indicate that the current is in the biased lock (look at the above picture), if it is biased under the thread Once you enter the synchronization code, you only need to compare whether the thread id of the Markword is equal to the current thread id. If they are equal, you can enter the synchronization code execution without doing any operations. If they are not equal after the comparison, it means that there are other threads competing for locks, and synchronized will be upgraded to lightweight. lock. In this process, there is no need to switch between kernel mode and user mode at the operating system level, reducing resource consumption caused by switching threads.
    • Expansion process: When another thread enters, the bias lock will be upgraded to a lightweight lock. For example, thread A is a biased lock. When thread B enters, it will become a lightweight lock. As long as there are two threads, it will be upgraded to a lightweight lock .

Let's take a look at the lock state of the bias lock in the code below.

package com.duyang.base.basic.markword;

import lombok.SneakyThrows;
import org.openjdk.jol.info.ClassLayout;

/**
 * @author :jiaolian
 * @date :Created in 2020-12-19 11:25
 * @description:markword测试
 * @modified By:
 * 公众号:叫练
 */
public class MarkWordTest {

    private static Fruit fruit = new Fruit();

    public static void main(String[] args) throws InterruptedException {
        Task task = new Task();
        Thread threadA = new Thread(task);
        Thread threadB = new Thread(task);
        Thread threadC = new Thread(task);
        threadA.start();
        //threadA.join();
        //threadB.start();
        //threadC.start();
    }

    private static class Task extends Thread {

        @SneakyThrows
        @Override
        public void run() {
            synchronized (fruit) {
                System.out.println("==================="+Thread.currentThread().getId()+" ");
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.print(ClassLayout.parseInstance(fruit).toPrintable());
            }
        }
    }
}

The above code starts thread A, and the console output is as shown in the figure below. The 3 bits marked in red are 101 respectively, the high 1 means the bias lock, and 01 is the bias lock identification bit. Conforms to the situation of the bias lock identification.

image.png

  • Lightweight lock.
    • Upgrade process: After the thread is running to acquire the lock, it will create a lock record in the stack frame and copy MarkWord to the lock record, and then point MarkWord to the lock record. If the current thread holds the lock, other threads will enter again, and other threads will cas spins until the lock is acquired. Lightweight locks are suitable for multi-threaded alternate execution and are highly efficient (cas only consumes cpu, which I talked about in detail in an article on cas principles.).
    • Expansion process: There are two situations that will expand into a heavyweight lock. One case is that the cas spins 10 times and has not acquired the lock. In the second case, other threads are acquiring locks in cas, and the third thread competes to acquire locks, and the locks will expand into heavyweight locks.

Below we code to test the lock state of the lightweight lock.

Open 23 lines to 24 lines of code, execute threads A, B, my purpose is to execute thread AB sequentially, so I execute threadA.join() in the code first, let thread A finish execution first, and then execute thread B, as shown below As shown in the MarkWord lock state change, thread A is initially deviated by the lock indicated by 101, and the execution thread B becomes a lightweight lock, and the lock state becomes 00, which conforms to the lightweight lock state. The proof is complete.

image.png

  • Heavyweight lock . The upgraded heavyweight lock is irreversible, which means that the heavyweight lock can no longer become a lightweight lock.

Open 25 lines of code, execute threads A, B, C, my purpose is to execute thread A first, execute threadA.join() in the code, let thread A finish execution first, and then execute thread BC at the same time, as shown in the figure below Show the change of the MarkWord lock state. Thread A is biased to lock at the beginning, and then executes thread BC at the same time. Because of fierce competition, it belongs to the second case of lightweight lock expansion condition. When other threads are acquiring locks in cas, the third thread Competing for locks will expand and become heavyweight locks. At this time, the BC thread lock status has become 10, which is in line with the heavyweight lock status. The expansion of the heavyweight lock has been proved.

image.png

 

 

So far, we have proved the lock status during the synchronized lock upgrade process in the form of code, and hope it will be helpful to you. The figure below is a summary of myself.

image.png

 

to sum up


Multi-threaded synchronization has always been a very important topic, and it is also a common test point in interviews. I hope everyone can understand and master it as soon as possible, share it with you, and hope you like it!

I’m called Lian, call more Lian , welcome everyone to discuss and exchange with me, I will reply to you as soon as possible, like to like and follow! The public account [Calling Lian].

 

Awesome.gif

Guess you like

Origin blog.csdn.net/duyabc/article/details/111468664