Java--synchronized and lock of multithreading; deadlock (4)

Java--multithreading concurrency, parallelism, process, thread (1) - MinggeQingchun's Blog - CSDN Blog

Java--Multi-thread Termination/Interruption Thread (2) - MinggeQingchun's Blog - CSDN Blog - Java Interruption Thread

Java--join, yield, sleep of multithreading; thread priority; timer; daemon thread (3) - MinggeQingchun's Blog - CSDN Blog

Java--Multi-threaded producer consumer mode; thread pool ExecutorService (5) - MinggeQingchun's Blog - CSDN Blog

one, synchronized

Before understanding synchronized, let's look at a thread-unsafe example

For example, if there is 10,000 yuan in an account, two people withdraw money at the same time, which will cause the balance to be wrong, or the money withdrawn is more than the amount in the account

public class AccountThreadTest {
    public static void main(String[] args) {
        Account account = new Account("xiaoming",10000.0);

        AccountThread t1 = new AccountThread(account);
        AccountThread t2 = new AccountThread(account);

        t1.setName("t1");
        t2.setName("t2");
        t1.start();
        t2.start();
    }
}

class AccountThread extends Thread{
    private Account account;

    public AccountThread(Account account){
        this.account = account;
    }

    @Override
    public void run() {
        double money = 5000.0;

        account.withDraw(money);

        System.out.println(Thread.currentThread().getName() + "在" + account.getAccount() + "账户成功取款" + money + ";余额为" + account.getBalance());
    }
}

class Account {
    private String account;
    private Double balance;

    public Account(){

    }

    public Account(String account,Double balance){
        this.account = account;
        this.balance = balance;
    }

    public String getAccount() {
        return account;
    }

    public void setAccount(String account) {
        this.account = account;
    }

    public Double getBalance() {
        return balance;
    }

    public void setBalance(Double balance) {
        this.balance = balance;
    }

    //取钱
    public void withDraw(Double money){
        Double afterMoney = this.getBalance() - money;

        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        this.setBalance(afterMoney);
    }
}

The output is as follows

t2在xiaoming账户成功取款5000.0;余额为5000.0
t1在xiaoming账户成功取款5000.0;余额为5000.0

Threads t1 and t2 respectively withdraw 5,000 from the same account with a total amount of 10,000, but the balance is still 5,000, which leads to data insecurity under concurrent multi-threading

(1) Security issues in a multi-threaded concurrent environment

1. Multi-threaded concurrent data security issues

After the following three conditions are met, there will be thread safety issues

1. Multi-threaded concurrency

2. There is shared data

3. Shared data has modification behavior

2. Solution to multi-threaded concurrent data security problem: thread synchronization mechanism

The thread synchronization mechanism is that threads are queued for execution, and part of the execution efficiency will be sacrificed when threads are queued for execution, but data security is the first priority, and only data security can talk about efficiency; data is not safe, and efficiency is out of the question.

In the Java language, the main means of ensuring thread safety is locking, and there are two main types of locks in Java: synchronized and Lock

(2) synchronized

Synchronized is a keyword in Java. Its implementation is based on jvm instructions. Before JDK1.5, we used synchronized to achieve thread synchronization without exception when writing concurrent programs, and synchronized was implemented in JDK1 Before .5, the overhead of synchronization was high and the efficiency was low. Therefore, after JDK1.5, the Lock interface at the code level (synchronized is at the jvm level) was introduced to realize the synchronization lock with the same function as synchronized

First look at synchronized

The official explanation of synchronized is as follows

Synchronized synchronization methods can support the use of a simple strategy to prevent thread interference and memory consistency errors: if an object is visible to multiple threads, all reads or writes to the object variables are done through synchronized methods.

Simply put, the role of Synchronized is the most commonly used and simplest method to solve concurrency problems in Java. It can ensure that at most one thread executes synchronization code at the same time, thereby ensuring the effect of concurrency security in a multi-threaded environment. If a piece of code is modified by Synchronized, then this code will be executed atomically. When multiple threads are executing this code, they are mutually exclusive, will not interfere with each other, and will not execute at the same time

The working mechanism of Synchronized is to use a lock in a multi-threaded environment. When the first thread executes, acquire the lock before it can be executed. Once acquired, it will monopolize the lock until the execution is completed or it will not be released under certain conditions. This lock, other threads can only block and wait until the lock is released

Synchronized is a keyword in Java, natively supported by Java, and is the most basic synchronization lock

The objects modified by synchronized are as follows: 

1. Modify a code block. The modified code block is called a synchronous statement block. Its scope of action is the code enclosed in braces {}, and the object of action is the object that calls this code block.

2. Modify a method. The modified method is called a synchronous method. Its scope of action is the entire method, and the object of action is the object that calls this method.

3. To modify a static method, the scope of its effect is the entire static method, and the object of action is all objects of this class

4. To modify a class, the scope of its effect is the part enclosed in brackets after synchronized, and the main object of action is all objects of this class

1. Modify the common method

/**
 * synchronized 修饰普通方法
 */
public synchronized void method() {
    // ....
}

When synchronized modifies an ordinary method, the modified method is called a synchronized method, and its scope of action is the entire method, and the object of action is the object calling this method this

2. Modified static method

/**
 * synchronized 修饰静态方法
 */
public static synchronized void staticMethod() {
    // .......
}

When synchronized modifies a static method, its scope of action is the entire program, and this lock is mutually exclusive for all objects that call this lock 

For static methods, synchronized locking is global, that is, during the entire program running, all objects that call this static method are mutually exclusive, while ordinary methods are aimed at the object level, and different objects correspond to different locks

Static method locking is global and targets all callers; common method locking is at the object level, and different objects have different locks

Object lock: 1 lock for 1 object, 100 locks for 100 objects
Class lock: 100 objects, or just 1 class lock

3. Modify the code block

In daily development, the most commonly used is to lock the code block instead of the method, because locking the method is equivalent to locking the entire method. In this case, the granularity of the lock is too large, and the execution of the program Performance will be affected, so under normal circumstances, we will use synchronized to lock the code block, and its implementation syntax is as follows

public void classMethod() throws InterruptedException {
    // 前置代码...
    
    // 加锁代码
    synchronized (SynchronizedUsage.class) {
        // ......
    }
    
    // 后置代码...
}

Compared with the modified method, the modified code block needs to manually specify the locked object, and the locked object is usually expressed in the form of this or xxx.class

// 加锁某个类
synchronized (SynchronizedUsage.class) {
    // ......
}

// 加锁当前类对象
synchronized (this) {
    // ......
}

this VS class

Using synchronized to lock this and xxx.class is completely different. When locking this, it means that the current object is used for locking, and each object corresponds to a lock; when using xxx.class to lock, it means Use a class (rather than a class instance) to lock, which is at the application level and is globally effective

It can be distinguished from the following four examples

/**
 doOther方法执行的时候需要等待doSome方法的结束吗?
 不需要,因为doOther()方法没有synchronized
 */
public class SynchronizedExam {
    public static void main(String[] args) {
        MyClass myClass = new MyClass();

        MyThread t1 = new MyThread(myClass);
        t1.setName("t1");

        MyThread t2 = new MyThread(myClass);
        t2.setName("t2");

        t1.start();
        // 睡眠的作用:保证t1线程先执行
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        t2.start();
    }
}

class MyThread extends Thread{
    private MyClass myClass;

    public MyThread(){
    }

    public MyThread(MyClass myClass){
        this.myClass = myClass;
    }

    @Override
    public void run() {
        if (Thread.currentThread().getName().equals("t1")){
            myClass.doSome();
        }
        if (Thread.currentThread().getName().equals("t2")){
            myClass.doOther();
        }
    }
}

class MyClass {
    public synchronized void doSome(){
        System.out.println("doSome begin");
        try {
            Thread.sleep(1000 * 5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("doSome over");
    }

    public void doOther(){
        System.out.println("doOther begin");
        System.out.println("doOther over");
    }
}

output

doSome begin
doOther begin
doOther over
doSome over
/**
 doOther方法执行的时候需要等待doSome方法的结束吗?
 需要,doOther()方法有synchronized
 */
public class SynchronizedExam {
    public static void main(String[] args) {
        MyClass myClass = new MyClass();

        MyThread t1 = new MyThread(myClass);
        t1.setName("t1");

        MyThread t2 = new MyThread(myClass);
        t2.setName("t2");

        t1.start();
        // 睡眠的作用:保证t1线程先执行
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        t2.start();
    }
}

class MyThread extends Thread{
    private MyClass myClass;

    public MyThread(){
    }

    public MyThread(MyClass myClass){
        this.myClass = myClass;
    }

    @Override
    public void run() {
        if (Thread.currentThread().getName().equals("t1")){
            myClass.doSome();
        }
        if (Thread.currentThread().getName().equals("t2")){
            myClass.doOther();
        }
    }
}

class MyClass {
    // 普通的同步方法 锁的调用者,如 this对象,obj对象; Object obj = new Object();
    public synchronized void doSome(){
        System.out.println("doSome begin");
        try {
            Thread.sleep(1000 * 5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("doSome over");
    }

    public synchronized void doOther(){
        System.out.println("doOther begin");
        System.out.println("doOther over");
    }
}

output

doSome begin
doSome over
doOther begin
doOther over
/**
 doOther方法执行的时候需要等待doSome方法的结束吗?
 不需要,因为MyClass对象是两个,两把锁
 */
public class SynchronizedExam {
    public static void main(String[] args) {
        MyClass myClass1 = new MyClass();
        MyClass myClass2 = new MyClass();

        MyThread t1 = new MyThread(myClass1);
        t1.setName("t1");

        MyThread t2 = new MyThread(myClass2);
        t2.setName("t2");

        t1.start();
        // 睡眠的作用:保证t1线程先执行
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        t2.start();
    }
}

class MyThread extends Thread{
    private MyClass myClass;

    public MyThread(){
    }

    public MyThread(MyClass myClass){
        this.myClass = myClass;
    }

    @Override
    public void run() {
        if (Thread.currentThread().getName().equals("t1")){
            myClass.doSome();
        }
        if (Thread.currentThread().getName().equals("t2")){
            myClass.doOther();
        }
    }
}

class MyClass {
    // 普通的同步方法 锁的调用者,如 this对象,obj对象; Object obj = new Object();
    public synchronized void doSome(){
        System.out.println("doSome begin");
        try {
            Thread.sleep(1000 * 5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("doSome over");
    }

    public synchronized void doOther(){
        System.out.println("doOther begin");
        System.out.println("doOther over");
    }
}

output

doSome begin
doOther begin
doOther over
doSome over
/**
 doOther方法执行的时候需要等待doSome方法的结束吗?
 需要,因为静态方法是类锁,不管创建了几个对象,类锁只有1把
 */
public class SynchronizedExam {
    public static void main(String[] args) {
        MyClass myClass1 = new MyClass();
        MyClass myClass2 = new MyClass();

        MyThread t1 = new MyThread(myClass1);
        t1.setName("t1");

        MyThread t2 = new MyThread(myClass2);
        t2.setName("t2");

        t1.start();
        // 睡眠的作用:保证t1线程先执行
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        t2.start();
    }
}

class MyThread extends Thread{
    private MyClass myClass;

    public MyThread(){
    }

    public MyThread(MyClass myClass){
        this.myClass = myClass;
    }

    @Override
    public void run() {
        if (Thread.currentThread().getName().equals("t1")){
            myClass.doSome();
        }
        if (Thread.currentThread().getName().equals("t2")){
            myClass.doOther();
        }
    }
}

class MyClass {
    // 静态的同步方法 锁的是 Class 类模板(.class文件;一个类只有唯一的一份.class文件)
    // synchronized 锁的对象是方法的调用者!
    // static 静态方法
    // 类一加载就有了!锁的是Class
    public synchronized static void doSome(){
        System.out.println("doSome begin");
        try {
            Thread.sleep(1000 * 5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("doSome over");
    }

    public synchronized static void doOther(){
        System.out.println("doOther begin");
        System.out.println("doOther over");
    }
}

output

doSome begin
doSome over
doOther begin
doOther over

For the above example of withdrawing money, we only need to add a lock to the shared object in Account

class Account {
    private String account;
    private Double balance;

    // 实例变量Account对象是多线程共享的,Account对象中的实例变量obj也是共享的)
    Object obj = new Object();

    public Account(){

    }

    public Account(String account,Double balance){
        this.account = account;
        this.balance = balance;
    }

    public String getAccount() {
        return account;
    }

    public void setAccount(String account) {
        this.account = account;
    }

    public Double getBalance() {
        return balance;
    }

    public void setBalance(Double balance) {
        this.balance = balance;
    }

    /**
     线程同步机制的语法是:
     synchronized(){
        // 线程同步代码块
     }
     被synchronized修饰的代码块及方法,在同一时间,只能被单个线程访问
     synchronized()小括号中传的“数据”必须是多线程共享的数据,才能达到多线程排队

     ()中参数
     假设t1、t2、t3、t4、t5,有5个线程
     希望t1 t2 t3排队,t4 t5不需要排队
     在()中写一个t1 t2 t3共享的对象,而这个对象对于t4 t5来说不是共享的

     synchronized()执行原理
     1、假设t1和t2线程并发,开始执行代码时,有一个先后顺序
     2、假设t1先执行,遇到了synchronized,t1自动找“共享对象”的对象锁,
     找到之后并占有这把锁,然后执行同步代码块中的程序,在程序执行过程中一直
     占有这把锁的,直到同步代码块代码结束,这把锁才会释放
     3、假设t1已经占有这把锁,此时t2也遇到synchronized关键字,也会去占有
     共享对象的这把锁,结果这把锁被t1占有,t2只能在同步代码块外面等待t1的结束,
     直到t1把同步代码块执行结束了,t1会归还这把锁,此时t2终于等到这把锁,然后
     t2占有这把锁之后,进入同步代码块执行程序
     */

    //取钱
    public void withDraw(Double money){
//        Object obj2 = new Object();

//        synchronized (obj) {
        //synchronized ("abc") { // "abc"在字符串常量池当中,会被所有线程共享
        //synchronized (null) { // 报错:空指针
//        synchronized (obj2) { // obj2是局部变量,不是共享对象
        synchronized (this){
            Double afterMoney = this.getBalance() - money;

            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            this.setBalance(afterMoney);
        }
    }
}

(3) Synchronized() execution principle

1. Assuming that t1 and t2 threads are concurrent, there is a sequence when starting to execute the code

2. Assuming that t1 is executed first, when synchronized is encountered, t1 automatically finds the object lock of the "shared object", finds it and holds the lock, and then executes the program in the synchronization code block, and holds the lock all the time during the program execution process. This lock will not be released until the end of the synchronized code block code

3. Assuming that t1 already owns this lock, t2 also encounters the synchronized keyword at this time, and will also occupy this lock of the shared object. As a result, this lock is occupied by t1, and t2 can only wait for t1 outside the synchronized code block End, until t1 finishes executing the synchronous code block, t1 will return the lock, at this time t2 finally waits for the lock, and then t2 takes possession of the lock, then enters the synchronous code block execution program

Two, lock

Starting from JDK 5.0, Java provides a more powerful thread synchronization mechanism - synchronization is achieved by explicitly defining a synchronization lock object. Synchronization locks use Lock objects as

The java.util.concurrent.locks.Lock interface is a tool for controlling access to shared resources by multiple threads

The lock provides exclusive access to shared resources. Only one thread can lock the Lock object at a time. Before the thread starts to access the shared resource, it should obtain the Lock object first.

The ReentrantLock class implements Lock, which has the same concurrency and memory semantics as synchronized. In implementing thread-safe control, ReentrantLock is more commonly used, which can explicitly lock and release locks

As follows, simulate the ticket purchase process

public class LockTest {
    public static void main(String[] args) {
        BuyTicketThread buyTicketThread = new BuyTicketThread();

        Thread t1 = new Thread(buyTicketThread);
        Thread t2 = new Thread(buyTicketThread);
        Thread t3 = new Thread(buyTicketThread);

        t1.setName("小明");
        t2.setName("小李");
        t3.setName("小张");

        t1.start();
        t2.start();
        t3.start();
    }
}

class BuyTicketThread implements Runnable{
    private int ticketNum = 10;
    private boolean flag = true;

    @Override
    public void run() {
        while (flag) {
            buy();
        }
    }

    //synchronized 同步方法,锁的是this
    private /*synchronized*/ void buy() {
        if (ticketNum <= 0){
            flag = false;
            return;
        }

        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "购买了第" + ticketNum-- + "张票");
    }
}

output

小李购买了第9张票
小张购买了第10张票
小明购买了第8张票
小张购买了第5张票
小明购买了第6张票
小李购买了第7张票
小张购买了第4张票
小明购买了第2张票
小李购买了第3张票
小张购买了第0张票
小明购买了第-1张票
小李购买了第1张票

If it is not locked, it will cause multiple different threads to buy the same ticket, or even 0 and negative numbers; if you add synchronized modification to the ticket buying method, you can ensure that the ticket buying is normal

import java.util.concurrent.locks.ReentrantLock;

public class LockTest {
    public static void main(String[] args) {
        BuyTicketThread buyTicketThread = new BuyTicketThread();

        Thread t1 = new Thread(buyTicketThread);
        Thread t2 = new Thread(buyTicketThread);
        Thread t3 = new Thread(buyTicketThread);

        t1.setName("小明");
        t2.setName("小李");
        t3.setName("小张");

        t1.start();
        t2.start();
        t3.start();
    }
}

class BuyTicketThread implements Runnable{
    private int ticketNum = 10;

    //定义lock锁
    private final ReentrantLock lock = new ReentrantLock();

    @Override
    public void run() {
        while (true) {
            try {
                //加锁
                lock.lock();
                if (ticketNum > 0){
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "购买了第" + ticketNum-- + "张票");
                }
            }finally {
                //解锁
                lock.unlock();
            }
        }
    }
}

The lock locks the Lock object. When its lock method is called, a flag state in the Lock class will be increased by 1 (state is actually a variable in the AbstractQueuedSynchronizer class, which is the parent class of an internal class in Lock. ), when the lock is released, the state is decremented by 1 (the operation of adding 1 and decrementing 1 is to achieve reentrancy)

Note:

Lock is an explicit lock, and the lock must be closed manually (manually open and close the lock, forgetting to close the lock will cause deadlock) 

Lock must be manually closed and released, and must be released in the finally clause

Source code comments in the ReentrantLock class

3. The difference between Synchronized and Lock

1. Lock is an interface; synchronized is a built-in keyword in Java

2. Lock can judge whether the lock has been acquired; synchronized cannot judge the status of the lock

3. Lock is an explicit lock, and the lock must be closed manually (manually opening and closing the lock, forgetting to close the lock will cause deadlock ); synchronized is an implicit lock, which will automatically release the lock, and automatically release it out of scope

4. Lock only has code block locks, which are suitable for a large number of synchronized code blocks; synchronized has code block locks and method locks, which are suitable for a small amount of code

5. Using the Lock lock, the JVM will spend less time scheduling threads, and the performance is better, which does not necessarily cause threads to queue up and wait for blocking. And it has better scalability (providing more subclasses); synchronized will cause threads to queue up and wait for blocking

6. Lock, reentrant lock, can judge the lock state, unfair; synchronized reentrant lock, uninterruptible, unfair

use priority

Lock > Synchronous code block (has entered the method body and allocated corresponding resources) > Synchronous method (outside the method body)

Fourth, the thread safety issues of the three major variables in Java

1. Three variables in Java

1. Instance variables: in the heap

2. Static variables: in the method area

3. Local variables: on the stack

Local variables can never be thread-safe; because local variables are not shared (one stack per thread). Local variables are on the stack. So local variables are never shared

 Instance variables are in the heap, and there is only one heap

 Static variables are in the method area, and there is only one method area

Both the heap and the method area are shared by multiple threads, so there may be thread safety issues

Local variables + constants: no thread safety issues

Member variables: there may be thread safety issues

2. Use local variables

Recommended use: StringBuilder (thread-unsafe)

Because local variables do not have thread safety issues

Choose StringBuilder; StringBuffer (thread-safe, the methods in the source code are modified with synchronized) is relatively inefficient

ArrayList is not thread safe

Vector is thread safe

HashMap HashSet is not thread safe

Hashtable is thread safe.

3. Solve thread safety issues during development

We don't choose thread synchronization synchronized at the beginning of development; synchronized will reduce the execution efficiency of the program and the user experience is not good. The user throughput of the system is reduced. Poor user experience. Choose the thread synchronization mechanism as a last resort

The first solution: try to use local variables instead of "instance variables and static variables"

The second solution: If it must be an instance variable, then you can consider creating multiple objects, so that the memory of the instance variable is not shared (one thread corresponds to 1 object, 100 threads correspond to 100 objects, objects are not shared, there is no data security issue)

The third solution: If local variables cannot be used and multiple objects cannot be created, at this time you can only choose synchronized (thread synchronization mechanism)

Five, deadlock

Thread 1 holds resource B, and thread 2 holds resource A. They both want to apply for each other's resources at the same time, so these two threads will wait for each other and enter a deadlock state

There will be no error reporting or abnormality in the deadlock phenomenon. The program has been deadlocked there, and it is difficult to find out and debug

1. Necessary conditions for deadlock

For a deadlock to occur, the following four conditions must be met:

1. Mutual exclusion condition: the resource is only occupied by one thread at any time

2. Request and hold conditions: When a process is blocked due to requesting resources, it will not let go of the obtained resources

3. Non-deprivation condition: The resources obtained by the thread cannot be forcibly deprived by other threads before they are used up, and the resources are released only after they are used up

4. Circular waiting condition: a number of processes form a head-to-tail circular waiting resource relationship

2. Avoid thread deadlock

The four necessary conditions for deadlock are mentioned above. In order to avoid deadlock, you only need to destroy one of the four conditions for deadlock.

1. Destroy the mutual exclusion condition: We have no way to destroy this condition, because we use locks to make them mutually exclusive (critical resources require mutually exclusive access)

2. Destruction request and maintenance conditions: apply for all resources at one time

3. Destruction and non-deprivation conditions: when a thread occupying some resources further applies for other resources, if the application cannot be obtained, it can actively release the resources it occupies

4. Destruction of circular waiting conditions: prevent by applying for resources in sequence. Apply for resources in a certain order, and release resources in reverse order. break loop wait condition

//死锁:
public class DeadLock {
    public static void main(String[] args) {
        Object obj1 = new Object();
        Object obj2 = new Object();

        MyThread1 t1 = new MyThread1(obj1,obj2);
        MyThread2 t2 = new MyThread2(obj1,obj2);
        t1.setName("t1");
        t2.setName("t2");
        t1.start();
        t2.start();
    }
}

class MyThread1 extends Thread{
    private Object obj1;
    private Object obj2;

    public MyThread1(){
    }

    public MyThread1(Object obj1,Object obj2){
        this.obj1 = obj1;
        this.obj2 = obj2;
    }

    @Override
    public void run() {
        synchronized (obj1){

            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            synchronized (obj2){

            }
        }
    }
}

class MyThread2 extends Thread{
    private Object obj1;
    private Object obj2;

    public MyThread2(){
    }

    public MyThread2(Object obj1,Object obj2){
        this.obj1 = obj1;
        this.obj2 = obj2;
    }

    @Override
    public void run() {
        synchronized (obj2){

            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            synchronized (obj1){

            }
        }
    }
}

reference link

https://www.jb51.net/article/244365.htm

Guess you like

Origin blog.csdn.net/MinggeQingchun/article/details/127272913