Abstract: When writing multithreaded concurrent programs, I clearly locked the shared resource. Why does it still go wrong? What went wrong? Actually, what I’m trying to say is: Are you locking correctly?
This article is shared by Huawei cloud community “[High Concurrency] Weird locking problem in high concurrency environment (The lock you add may not be secure)”, author: Glacier.
We know that in concurrent programming, you cannot use multiple locks to protect the same resource, because this does not achieve the effect of mutually exclusive threads, there are thread safety issues. Instead, you can use the same lock to protect multiple resources. So how do you protect multiple resources with the same lock? How do we know if the lock we put on the program is secure? Let’s dig deeper into these questions!
Analysis of the scene
When we analyze how to use the same lock to protect multiple resources in multi-threading, we can combine it with specific business scenarios, such as whether there is a direct business relationship between multiple resources to be protected. If the resources to be protected have no direct business relationship, how to lock them? If there is a direct business relationship, how can it be locked? Next, we will go further along these two directions.
Scenarios without direct business relationships
For example, in our Alipay account, there are payment operations for the balance and modification of the account password. In essence, there is no direct business relationship between the two operations, at which point we can solve the concurrency problem by assigning different locks for the account balance and the account password.
For example, in the AlipayAccount class of AlipayAccount, there are two member variables, namely the balance of the account and the password of the account. The pay() method for the payment operation and the getBalance() method for the check balance operation access the member variable balance in the account. For this, we can create a balanceLock lock object to protect the balance resource. In addition, the updatePassword() method for changing the password and the getPassowrd() method for viewing the password access the member variable password in the account, for which we can create a passwordLock lock object to protect the Password resource.
The specific code is shown below.
Public class AlipayAccount{private Final Object balanceLock = new Object(); public class AlipayAccount{private final Object balanceLock = new Object(); Private final Object passwordLock = new Object(); Private Integer balance; Private String password; Public void pay(Integer money){synchronized(balanceLock){if(this.balance >= money){this.balance -= money; }}} public Integer getBalance(){synchronized(balanceLock){return this.balance; }} // Change the password public void updatePassword(String password){synchronized(passwordLock){this.password = password; }} public String getPassword(){synchronized(passwordLock){return this.password; }}}Copy the code
Here, we can also use a mutex to protect the balance and Password resources, for example, both balanceLock and passwordLock. You can even use this object or simply prefix each method with the synchronized keyword.
However, if they all use the same lock object, then the performance of the program will be poor. Operations that do not have a direct business relationship are executed serially, which defeats the purpose of concurrent programming. In fact, we use two lock objects to protect the balance resource and the Password resource, and payment and account password change can be done in parallel.
Scenarios where direct business relationships exist
For example, we use Alipay to transfer money. Let’s say account A transfers 100 to account B, so account A loses $100 and account B gains $100. The two accounts have a direct business relationship in the business. For example, the TansferAccount class below, which has a member variable balance and a transfer() method, looks like this.
public class TansferAccount{ private Integer balance; public void transfer(TansferAccount target, Integer transferMoney){ if(this.balance >= transferMoney){ this.balance -= transferMoney; target.balance += transferMoney; }}}Copy the code
In the above code, how to ensure that the transfer operation does not have concurrency problems? Many times our first reaction is to lock the Transfer () method, as shown in the following code.
public class TansferAccount{ private Integer balance; public synchronized void transfer(TansferAccount target, Integer transferMoney){ if(this.balance >= transferMoney){ this.balance -= transferMoney; target.balance += transferMoney; }}}Copy the code
Let’s take a closer look. Is the code above ** really secure? ! Synchronized (this) : synchronized(this) : synchronized(this) : synchronized(this) : synchronized(this) : synchronized(this) Here, we have a suddenly enlightened feeling. Synchronized (this) is a synchronized(this) lock that protects only this.balance resources, not target.balance resources.
We can use the following figure to represent this logic.
We can also see from the above figure that this lock object only protects this.balance resources, not target.balance resources.
Next, let’s look at A scenario: suppose there are three accounts A, B, and C, each with A balance of 200. At this point, we use two threads to perform two transfer operations respectively: account A transfers 100 to account B, and account B transfers 100 to account C. In theory, account A has A balance of 100, account B has A balance of 200, and account C has A balance of 300.
Is it really so? Let’s assume that thread A and thread B are executing simultaneously on two different cpus. Thread A performs the transfer of 100 from account A to account B, and thread B performs the transfer of 100 from account B to account C. Are two threads mutually exclusive? Apparently not. According to the TansferAccount code, thread A locks the instance of account A and thread B locks the instance of account B. Therefore, thread A and thread B can enter the Transfer () method at the same time. At this point, both thread A and thread B can read account B’s balance of 200. After both threads complete the transfer operation, B’s account balance may be 300 or 100, but not 200.
Why is that? Thread A and thread B simultaneously read the balance of account B as 200. If the transfer operation of thread A is later than the transfer operation of thread B writes to balance, the balance of account B is 300. If thread A’s transfer operation is written to balance before thread B’s transfer operation, account B has A balance of 100. In any case, account B’s balance is never going to be 200.
In summary, the TansferAccount code simply does not solve the concurrency problem!
Lock correctly
If we want to lock multiple resources involved in a transfer operation, our lock must cover all resources that need to be protected.
In the previous TansferAccount class, this is an object-level lock, which results in different locks being acquired by thread A and thread B. How can two threads share the same lock? !
There are many solutions. One simple way is to pass a balanceLock lock object into the constructor of TansferAccount, and pass the same balanceLock lock object each time when creating TansferAccount. And use balanceLock lock object to lock in transfer method. In this way, all created TansferAccount class objects share the balanceLock lock. The code is shown below.
public class TansferAccount{ private Integer balance; private Object balanceLock; private TansferAccount(){} public TansferAccount(Object balanceLock){ this.balanceLock = balanceLock; } public void transfer(TansferAccount target, Integer transferMoney){ synchronized(this.balanceLock){ if(this.balance >= transferMoney){ this.balance -= transferMoney; target.balance += transferMoney; }}}}Copy the code
Which begs the question: Is this really the perfect solution? !
The above code solves the concurrency problem of the transfer operation, but is it really perfect? ! After careful analysis, we found that it was not as perfect as expected. Because TansferAccount requires that the same balanceLock object be passed in to create the TansferAccount object, if the balanceLock object is not the same as the balanceLock object, there is no guarantee of thread safety with concurrency! In a real project, the operation to create a TansferAccount object might be spread across several different projects, making it difficult to guarantee that the balanceLock object passed in is the same object.
Therefore, passing in the same balanceLock lock object when creating TansferAccount can solve the concurrency problem of transfer, but it cannot be effectively adopted in the actual project!
Are there any other plans? The answer is yes! Remember that when the JVM locks a Class, it creates a Class object that is shared with all instances of the Class. That is, the JVM guarantees that the Class object will be the same no matter how many instances of the Class are created.
At this point, we can think of the following way to lock the transfer operation.
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; }}}}Copy the code
We can use the following diagram to illustrate this logic.
This way, no matter how many TansferAccount objects are created, they all share the same lock, eliminating the concurrency problem for transfers.
Click to follow, the first time to learn about Huawei cloud fresh technology ~