[High Concurrency] How to optimize the locking method in high concurrency scenarios? I really understand after reading this! !

Write in front

Many times, when we involve locking operations in concurrent programming, are the locking operations on code blocks really reasonable? Are there any areas that need to be optimized?

Foreword

In " [High Concurrency], when optimizing the locking method, it was deadlocked! ! "In the article, we introduced the four necessary conditions when a deadlock occurs. Only when the four conditions are met at the same time can a deadlock occur. Among them, we used a way to apply for all resources at once when blocking requests and maintaining conditions . For example, in the process of completing the transfer operation, we apply for account A and account B at a time. After both accounts have been successfully applied, we then perform the transfer operation. Among them, in the transfer method we implemented, an endless loop was used to obtain resources cyclically until account A and account B were obtained at the same time. The core code is shown below.

//一次申请转出账户和转入账户,直到成功
while(!requester.applyResources(this, target)){
    //循环体为空
    ;
}

If the applyResources () method of the ResourcesRequester class is executed for a very short time, and the conflict caused by the program concurrency is not large, the program can obtain the transfer-out account and transfer-in account at the same time from several to dozens of times. This solution is feasible. of.

However, if the applyResources () method of the ResourcesRequester class takes a long time to execute, or the conflicts caused by program concurrency are relatively large, it may take thousands of cycles to obtain the transfer-out account and transfer-in account at the same time . This consumes too much CPU resources. At this time, this solution is not feasible.

So, is there any way to optimize this solution?

problem analysis

Since there is a problem with using the infinite loop to always obtain resources, let's think about it differently. When the thread executes and finds that the conditions are not met, can it let the thread enter the wait state? When the conditions are met, notify the waiting thread to execute again?

In other words, if the conditions required by the thread are not satisfied, we let the thread enter the waiting state; if the conditions required by the thread are satisfied, we then notify the waiting thread to execute again. In this way, you can avoid the problem that the program waits in a loop and consumes CPU.

So, the question is coming again! When the conditions are not met, how to make the thread wait? When the conditions are met, how to wake up the thread?

Yes, this is a problem! But solving this problem is also very simple. Simply put, it uses a thread's wait and notification mechanism.

Thread waiting and notification mechanism

We can use the thread's waiting and notification mechanism to optimize the problem of obtaining account resources in a loop when blocking requests and maintaining conditions . The specific waiting and notification mechanism is shown below.

The executing thread first acquires the mutex. If the required conditions are not met when the thread continues to execute, the mutex is released and enters the waiting state; when the thread continues to execute the required conditions are met, the waiting thread is notified and reacquired Mutually exclusive lock.

So, with all that said, does Java support this kind of thread waiting and notification mechanism? In fact, this question is a bit nonsense. Java, such an excellent (very powerful) language, certainly supports it, and it is relatively simple to implement.

Implementing thread waiting and notification mechanism with Java

Method to realize

In fact, there are many ways to use the Java language to implement the thread waiting and notification mechanism. Here I will simply enumerate one way. You can think and implement other ways by yourself. If you do n’t understand, you can ask me!

In the Java language, a simple way to implement the thread's wait and notification mechanism is to use synchronized and combine wait (), notify (), and notifyAll () methods.

Implementation principle

When we use synchronized locking, only one thread is allowed to enter the synchronized protected code block, that is, the critical section. If a thread enters the critical section, other threads will enter the blocking queue and wait. This blocking queue and synchronized mutex lock are in a one-to-one relationship, that is, a mutex lock corresponds to an independent blocking queue .

In concurrent programming, if a thread acquires a synchronized mutex, but does not meet the conditions to continue down, you need to enter a wait state. At this time, you can use the wait () method in Java to achieve. When the wait () method is called, the current thread will be blocked, and it will enter a wait queue to wait. The wait queue entered by calling the wait () method is also the wait queue of the mutex. Moreover, when the thread enters the waiting queue, it will release the mutex it has acquired, so that other threads have the opportunity to obtain the mutex and then enter the critical section. The whole process can be expressed as shown in the figure below.

When the thread execution conditions are met, you can use the notify () and notifyAll () methods provided by Java to notify the threads in the mutex lock queue. We can use the following figure to simply represent this process.

Here, the following items need to be noted:

(1) When the notify () and notifyAll () methods are used to notify the thread, when the notify () and notifyAll () methods are called, the execution conditions of the thread are met, but when the thread is actually executed, the conditions may no longer be satisfied. There are other threads that have entered the critical section for execution.

(2) When the notified thread continues to execute, it needs to acquire the mutex first, because the mutex has been released when the wait () method is called to wait.

(3) The queue operated by the wait (), notify () and notifyAll () methods is the waiting queue of the mutex lock. If synchronized is locked to this object, you must use this.wait (), this.notify () and this.notifyAll () method; if synchronized locks the target object, you must use target.wait (), target.notify () and target.notifyAll () methods.

(4) The premise of the call of wait (), notify () and notifyAll () methods is that the corresponding mutex has been acquired, that is to say, the wait (), notify () and notifyAll () methods are all in the synchronized method Or called in the code block. If three methods are called outside the synchronized method or outside the code block, or the locked object is this, and the three methods are called using the target object, the JVM will throw a java.lang.IllegalMonitorStateException exception.

Implementation

Implementation logic

Before implementation, we also need to consider the following issues:

  • Which mutex to choose

In the previous program, we have a singleton object of the ResourcesRequester class in the TansferAccount class, so we can use this as a mutex. Everyone needs to understand this point.

  • Conditions for threads to perform transfer operations

Neither the transfer-out account nor the transfer-in account has been allocated.

  • When does the thread enter the wait state

When the thread continues to execute the required conditions are not met, enter the wait state.

  • When to notify the waiting thread to execute

When a thread releases the account's resources, the waiting thread is notified to continue execution.

In summary, we can draw the following core code.

while(不满足条件){
    wait();
}

So, the question is coming! Why is the wait () method called in the while loop? Because when the wait () method returns, it is possible that the condition of the thread execution has changed, that is to say, the previous condition is satisfied, but it is no longer satisfied, so we must re-check whether the condition is met.

Implementation code

The code of our optimized ResourcesRequester class is shown below.

public class ResourcesRequester{
    //存放申请资源的集合
    private List<Object> resources = new ArrayList<Object>();
    //一次申请所有的资源
    public synchronized void applyResources(Object source, Object target){
        while(resources.contains(source) || resources.contains(target)){
            try{
                wait();
            }catch(Exception e){
                e.printStackTrace();
            }
        }
        resources.add(source);
        resources.add(targer);
    }
    
    //释放资源
    public synchronized void releaseResources(Object source, Object target){
        resources.remove(source);
        resources.remove(target);
        notifyAll();
    }
}

The code of the ResourcesRequesterHolder that generates the ResourcesRequester singleton object is shown below.

public class ResourcesRequesterHolder{
    private ResourcesRequesterHolder(){}
    
    public static ResourcesRequester getInstance(){
        return Singleton.INSTANCE.getInstance();
    }
    private enum Singleton{
        INSTANCE;
        private ResourcesRequester singleton;
        Singleton(){
            singleton = new ResourcesRequester();
        }
        public ResourcesRequester getInstance(){
            return singleton;
        }
    }
}

The code for the class that performs the transfer operation is shown below.

public class TansferAccount{
    //账户的余额
    private Integer balance;
    //ResourcesRequester类的单例对象
    private ResourcesRequester requester;
   
    public TansferAccount(Integer balance){
        this.balance = balance;
        this.requester = ResourcesRequesterHolder.getInstance();
    }
    //转账操作
    public void transfer(TansferAccount target, Integer transferMoney){
        //一次申请转出账户和转入账户,直到成功
        requester.applyResources(this, target))
        try{
            //对转出账户加锁
            synchronized(this){
                //对转入账户加锁
                synchronized(target){
                    if(this.balance >= transferMoney){
                        this.balance -= transferMoney;
                        target.balance += transferMoney;
                    }   
                }
            }
        }finally{
            //最后释放账户资源
            requester.releaseResources(this, target);
        }
    }
}

As you can see, when we notify the thread in the waiting state in the program, the notifyAll () method is used instead of the notify () method. What is the difference between the notify () method and the notifyAll () method?

The difference between notify () method and notifyAll () method

  • notify () method

Randomly notify a thread in the waiting queue.

  • notifyAll () method

Notify all threads in the waiting queue.

In the actual work process, if there are no special requirements, try to use the notifyAll () method. Because using the notify () method is risky, it may cause certain threads to never be notified!

Write at the end

If you find the article helpful to you, please search and follow the WeChat account of " Binghe Technology " on WeChat, and learn high-concurrency programming techniques with Binghe

Finally, attach the core skills knowledge map that concurrent programming needs to master. I wish you all to avoid detours when learning concurrent programming.

Guess you like

Origin www.cnblogs.com/binghe001/p/12740921.html