I’m going to talk about deadlocks and how to solve them using a classic money transfer problem.

If customer A needs to transfer money to customer B, customer A’s account is reduced by $100 and Customer B’s account is increased by $100.

We convert this into code description: There is an Account class Account, and the Account class has a transfer() method, which takes two parameters, the transfer Account and the transfer amount.

Example: 3-1 public class Account {private int balance; public void transfer(Account target,int amt) { this.balance -= amt; target.balance += amt; }}Copy the code

Example 3-1 has a member variable balance. How can we ensure that this member variable is thread-safe in concurrent cases? I’m sure you can make the following improvements to the code after you think about it:

Example: 3-2 Public class Account {private int balance; public synchronized void transfer(Account target,int amt) { this.balance -= amt; target.balance += amt; }}Copy the code

So what’s wrong with Synchronized? The default lock is this, which is the current object, but if we look closely at this code we can see where the problem is. There are two resources in the code area, a “this” for roll out and a “target” for roll in. Lock (this) can only lock the balance variable of the current account transferred out, but cannot lock the balance of the account transferred in. Therefore, if a thread operates on account B at this time (for example, customer B withdraws 100 yuan).

Now that we know what the problem is, the current account lock does not hold two resources, so can we use multiple locks to hold two resources?

Example: 3-3 Public class Account {private int balance; public void transfer(Account target,int amt) { synchronized(this) { synchronized(target) { this.balance -= amt; target.balance += amt; }}}}Copy the code

It seems that we solved the situation where the this lock could not hold two resources, and we did solve the problem, but a new problem arose. Let’s assume that A transfers money to B at the same time that B transfers money to A.

Logic to code level:

  • Transfer from A to B: First acquire the lock of object A and then attempt to acquire the lock of object B
  • Transfer from B to A: First acquire the lock of object B, and then try to acquire the lock of object A

At this point, we can see that A deadlock is formed. When A transfers money, A tries to get B’s lock by taking his lock, and B tries to get A’s lock by taking his lock, and then no one can get the lock of the other. This is A deadlock.

Deadlock prevention

A deadlock can occur only when all four conditions occur:

  1. Mutually exclusive. Shared resources X and Y can only be occupied by one thread
  2. Thread T1 has acquired shared resource X and does not release shared resource X while waiting for shared resource Y
  3. No preemption. Other threads cannot forcibly preempt resources held by thread T1
  4. A circular wait, in which thread T1 waits for resources held by thread T2, and thread T2 waits for resources held by thread T1, is called a circular wait

If we can break one of these, we can successfully avoid deadlocks. Because we need mutual exclusion for shared resources, the first condition cannot be broken, so we can use the following three conditions.

Solve the damn lock problem

Adjust the order in which locks are acquired

According to the deadlock analysis above, we know that the deadlock problem is caused by the inconsistent order of acquiring locks when transferring money at the same time, so we can adjust the order of acquiring locks to keep the same, so the deadlock problem can be solved.

  • A transfers money to B: A obtains its own lock and then tries to obtain the lock of B
  • B transfers to A: B first obtains A’s lock and then tries to obtain B’s own lock

Each account has a unique Id, which is usually incremented, and the order of locks can be determined by the size of the Id.

The code is as follows:

Example: 3-4 Public class Account {private int balance; private int id; public void transfer(Account target,int amt) { Account first = this; Account second = target; if(this.id < target.id) { first = target; second = this; } synchronized(first) { synchronized(second ) { this.balance -= amt; target.balance += amt; }}}}Copy the code

We can avoid deadlocks by adjusting the order in which locks are acquired so that each lock is acquired with the largest ID.

Assume that the ID of account A is 100 and that of account B is 101.

  • A transfers to B: B.ID >A.id first obtains B’s lock and then tries to obtain A’s lock
  • B transfers to A: B.ID > A.id first tries to acquire B’s lock (wait)
Setting Timeout waiver

Synchronized Lock is used in the above code and cannot be set to timeout. If you need to set timeout, you can use display Lock.

Example: 3-5 private Lock Lock = new ReentrantLock(); private int balance; public void transfer(Account target, int amt) { try { lock.tryLock(10, TimeUnit.MILLISECONDS); this.balance -= amt; target.balance += amt; } catch (InterruptedException e) { } finally { lock.unlock(); }}Copy the code

This method can wait for a lock for a fixed period of time, so the thread can voluntarily release all the locks it has acquired after the lock timeout. This way, deadlocks can also be effectively avoided.

Note: in the actual development, this is solved by using database transaction + optimistic lock. This example is intended only to demonstrate deadlocks.