[High Concurrency] Deadlock when optimizing the locking method! !

Write in front

Today, when optimizing the locking method of the program, there is a deadlock! ! Why exactly? ! After careful analysis, I finally found the reason.

Why do we need to optimize the locking method?

In " [High Concurrency] Strange Locking Problem in High Concurrency Environment (The lock you added may not be safe) ", we use TansferAccount.class object in the transfer class TansferAccount to lock the program as shown below.

public class TansferAccount{
    private Integer balance;
    public void transfer(TansferAccount target, Integer transferMoney){
        synchronized(TansferAccount.class){
        	if(this.balance >= transferMoney){
                this.balance -= transferMoney;
                target.balance += transferMoney;
            }   
        }
    }
}

This method does solve the concurrency problem of transfer operations, but is this method really desirable in a highly concurrent environment? Imagine if we use the above code to handle the transfer operation in a highly concurrent environment, because the TansferAccount.class object is created when the JVM loads the TansferAccount class, all TansferAccount instance objects will share a TansferAccount.class object. In other words, all TansferAccount instance objects are mutually exclusive when they execute the transfer () method! ! In other words, all transfer operations are serial! !

If all transfer operations are performed serially, the consequence is that the transfer operation from account C to account D can only be performed after the transfer from account A to account B is completed. If netizens around the world perform transfer operations together, and these transfer operations are performed serially, then the performance of the program is completely unacceptable! ! !

In fact, the transfer operation of account A for account B and the transfer operation of account C for account D can be performed in parallel. Therefore, we must optimize the locking method to improve the performance of the program! !

Initially optimize the locking method

Since direct TansferAccount.class is undesirable to lock the program in a highly concurrent environment, what should we do? !

Carefully analyze the above code business. The transfer operation of the above code involves the transfer out account this and the transfer account target, so we can lock the transfer out account this and the transfer account target, only two accounts are added Only when the locks are successful, the transfer operation is performed. In this way, the operation of transferring account A to account B and the operation of transferring account C to account D can be performed in parallel.

We can express the optimized logic as shown in the figure below.

14

Based on the above analysis, we can optimize the TansferAccount code as shown below.

public class TansferAccount{
    //账户的余额
    private Integer balance;
    //转账操作
    public void transfer(TansferAccount target, Integer transferMoney){
        //对转出账户加锁
        synchronized(this){
            //对转入账户加锁
            synchronized(target){
                if(this.balance >= transferMoney){
                    this.balance -= transferMoney;
                    target.balance += transferMoney;
                }   
            }
        }
    }
}

At this point, the code above looks okay, but is this really the case? I also hope that the program is perfect, but often it is not what we think! Yes, the above procedure there will be a deadlock, why there is a deadlock ah? Next, we began to analyze a wave.

Analysis of the problem of deadlock

The code in the TansferAccount class looks perfect, but the optimized locking method will cause a deadlock! ! ! This is the conclusion I personally tested! !

Regarding the deadlock, we can combine the improved TansferAccount class to give a simple scenario: suppose that there are two threads, thread A and thread B, running on two different CPUs at the same time. Thread A performs the transfer of account A to account B. Thread B Perform the transfer of account B to account A. When thread A and thread B execute synchronized (this) code, thread A acquires the lock of account A, and thread B acquires the lock of account B. When the synchronized (target) code is executed, when thread A tries to obtain the lock of account B, it is found that account B has been locked by thread B. At this time, thread A begins to wait for thread B to release the lock of account B; while thread B tries to obtain account A When the lock is found, account A has been locked by thread A. At this time, thread B begins to wait for thread A to release account A's lock.

In this way, thread A holds the lock of account A and waits for thread B to release the lock of account B, thread B holds the lock of account B and waits for thread A to release the lock of account A, and a deadlock occurs! !

Necessary conditions for deadlock

Before how to solve the deadlock, let's first look at the necessary conditions when a deadlock occurs. If a deadlock is to occur, there must be the following four necessary conditions, all of which are indispensable.

  • Mutually exclusive condition

For a period of time, a resource is only occupied by one thread. At this time, if another thread requests the resource, the requesting thread can only wait.

  • Inalienable condition

The resource obtained by a thread cannot be forcibly taken away by other threads before it is used up, that is, it can only be released by the thread that obtained the resource itself (it can only be actively released).

  • Request and hold conditions

The thread has kept at least one resource, but has made a new resource request, and the resource has been occupied by other threads. At this time, the requesting thread is blocked, but it keeps the resources it has obtained.

  • Loop wait condition

Since the above four conditions must exist for deadlocks to occur, can everyone think of how to prevent deadlocks?

Deadlock prevention

In concurrent programming, once a deadlock occurs, there is basically no particularly good solution, and in general, the application can only be restarted to solve it. Therefore, the best way to solve the deadlock is to prevent the deadlock.

When a deadlock occurs, there must be four necessary conditions for a deadlock. That is to say, if we write a program, as long as one of the four necessary conditions of "destruction" deadlock, we can avoid the occurrence of deadlock. Next, let's discuss how to "destruct" these four necessary conditions.

  • Break the mutex

The mutex condition is something we can't break, because we use locks for mutual exclusion between threads. This point needs special attention! ! ! !

  • Undermine inalienable conditions

The core of destroying the inalienable condition is to let the current thread actively release the occupied resources. On this point, synchronized cannot be done. We can use the Lock under the java.util.concurrent package to solve it. At this time, we need to modify the code of the TansferAccount class to be similar to the following.

public class TansferAccount{
    private Lock thisLock = new ReentrantLock();
    private Lock targetLock = new ReentrantLock();
    //账户的余额
    private Integer balance;
    //转账操作
    public void transfer(TansferAccount target, Integer transferMoney){
        boolean isThisLock = thisLock.tryLock();
        if(isThisLock){
            try{
                boolean isTargetLock = targetLock.tryLock();
                if(isTargetLock){
                    try{
                         if(this.balance >= transferMoney){
                            this.balance -= transferMoney;
                            target.balance += transferMoney;
                        }   
                    }finally{
                        targetLock.unlock
                    }
                }
            }finally{
                thisLock.unlock();
            }
        }
    }
}

There are two tryLock methods in Lock, as shown below.

  • tryLock () method

The tryLock () method has a return value, which means that it is used to try to acquire the lock. If the acquisition is successful, it returns true. If the acquisition fails (that is, the lock has been acquired by another thread), it returns false. Will return immediately. When you can't get the lock, you won't wait there forever.

  • tryLock(long time, TimeUnit unit)方法

The tryLock (long time, TimeUnit unit) method is similar to the tryLock () method, but the difference is that this method will wait for a certain time when the lock cannot be obtained, and if the lock cannot be obtained within the time limit, return false. If the lock is obtained at the beginning or within the waiting period, it returns true.

  • Destruction request and hold condition

To destroy the request and maintain the conditions, we can apply all the resources we need at one time. For example, in the process of completing the transfer operation, we apply for account A and account B at one time. After both accounts have been successfully applied, we can perform the transfer operation. . At this point, we need to create another ResourceRequester class that applies for resources. The role of this class is to apply for resources and release resources. At the same time, the TansferAccount class needs to hold a single instance object of the ResourcesRequester class. When we need to perform a transfer operation, we first apply to the ResourcesRequester for both the transfer-out account and the transfer-in account. After the application is successful, the two resources are locked; When the transfer operation is complete, release the lock and release the transfer-out account and transfer-in account resources requested by the ResourcesRequester class.

The code of ResourcesRequester class is shown below.

public class ResourcesRequester{
    //存放申请资源的集合
    private List<Object> resources = new ArrayList<Object>();
    //一次申请所有的资源
    public synchronized boolean applyResources(Object source, Object target){
        if(resources.contains(source) || resources.contains(target)){
            return false;
        }
        resources.add(source);
        resources.add(targer);
        return true;
    }
    
    //释放资源
    public synchronized void releaseResources(Object source, Object target){
        resources.remove(source);
        resources.remove(target);
    }
}

At this time, the code of the TansferAccount class is shown below.

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

    }
}
  • Break the loop wait condition

To destroy the loop waiting condition, you can apply for resources in a certain order by sorting resources, and then lock the resources in order, which can effectively avoid deadlocks.

For example, in our transfer operation, each account will often have a unique id value. When we lock account resources, we can apply for account resources in order of id value from small to large, and in order of id from small to large To lock the account, at this time, the program will no longer wait in a loop.

The program code is shown below.

public class TansferAccount{
    //账户的id
    private Integer id;
    //账户的余额
    private Integer balance;
    //转账操作
    public void transfer(TansferAccount target, Integer transferMoney){
        TansferAccount beforeAccount = this;
        TansferAccount afterAccount = target;
        if(this.id > target.id){
            beforeAccount = target;
            afterAccount = this;
        }
        //对转出账户加锁
        synchronized(beforeAccount){
            //对转入账户加锁
            synchronized(afterAccount){
                if(this.balance >= transferMoney){
                    this.balance -= transferMoney;
                    target.balance += transferMoney;
                }   
            }
        }
    }
}

to sum up

In concurrent programming, when using fine-grained locks to lock multiple resources, always pay attention to the problem of deadlocks. In addition, the easiest way to avoid deadlock is to prevent loop waiting conditions, set all the resources in the system flag bit, sort, and stipulate that all threads applying for resources must operate in a certain order to avoid deadlock.

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.

sandahexin_20200322

Guess you like

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