Java thread deadlock

Posted by journeyman73 on Fri, 11 Feb 2022 11:39:34 +0100

What is deadlock

Thread 1 holds resource A and thread 2 holds resource B. At this time, if thread 1 requests resource B, thread 2 requests resource A. Since resource A and resource B have been held by other threads, thread 1 and thread 2 have been unable to obtain the desired resources and fall into an infinite waiting state.

A program with inevitable deadlock

The following is a code to demonstrate:

/**
 * Example of inevitable deadlock
 */
public class MustDeadLock implements Runnable {

    int state = 1;
    static Object lock1 = new Object();
    static Object lock2 = new Object();

    public static void main(String[] args) {
        MustDeadLock mustDeadLock = new MustDeadLock();
        MustDeadLock mustDeadLock1 = new MustDeadLock();
        mustDeadLock.state = 0;
        new Thread(mustDeadLock).start();
        new Thread(mustDeadLock1).start();
    }

    @Override
    public void run() {
        try {
            if (state == 1) {
                synchronized (lock1) {
                    System.out.println(Thread.currentThread().getName()+"Get lock1");
                    Thread.sleep(500);
                    System.out.println(Thread.currentThread().getName()+"Ready to get lock2");
                    synchronized (lock2) {
                        System.out.println(Thread.currentThread().getName()+"Get lock2");
                    }
                }
            }
            if (state == 0) {
                synchronized (lock2) {
                    System.out.println(Thread.currentThread().getName()+"Get lock2");
                    Thread.sleep(500);
                    System.out.println(Thread.currentThread().getName()+"Ready to get lock1");
                    synchronized (lock1) {
                        System.out.println(Thread.currentThread().getName()+"Get lock1");
                    }
                }
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

Execution result:

Thread-0 Get lock2
Thread-1 Get lock1
Thread-1 Ready to get lock2
Thread-0 Ready to get lock1

Thread-0 gets lock2 and Thread-1 gets lock1. At this time, when Thread-1 is ready to obtain lock2, lock2 is held by thread-0, so Thread-1 falls into an infinite waiting state. Thread-0, too, has been waiting to get lock1.

Four necessary conditions for deadlock

Through the above examples, we analyze the four necessary conditions for the occurrence of deadlock.
1: Mutually exclusive condition. Both lock1 and lock2 can only be held by one thread at the same time.
2: Hold and request. Thread-0 holds a resource lock2, and then it requests another resource.
3: Conditions of non deprivation. The resource lock2 held by Thread-0 can only be released by itself. Threads cannot release resources not held by themselves.
4: Loop waiting. Thread-0 obtains lock2 and requests lock1. Thread-1 obtains lock1 and requests lock2. Thread-0 is waiting for the resources held by thread-1, and thread-1 is waiting for the resources held by thread-0. Thread-0 -->Thread-1 -->Thread-0.
In case of deadlock, none of the above four conditions is indispensable.

Deadlock problem in actual production

Bank transfer problem

There are two users, Zhang San and Li Si. Zhang San's account balance is 500 yuan and Li Si's account balance is 500 yuan. At the same time, Zhang San transferred 200 yuan to Li Si's account and Li Si transferred 200 yuan to Zhang San's account for 100 times. The following is the specific implementation.

/**
 * Thread deadlock caused by simulating two person transfer
 */
public class TransferMoney implements Runnable {

    Acount a;
    Acount b;
    int amount;

    public TransferMoney(Acount a, Acount b, int amount) {
        this.a = a;
        this.b = b;
        this.amount = amount;
    }

    public static void main(String[] args) {
        Acount acount = new Acount(500);
        Acount acount1 = new Acount(500);
        TransferMoney transferMoney = new TransferMoney(acount, acount1, 200);
        TransferMoney transferMoney2 = new TransferMoney(acount1, acount, 200);
        for (int i = 0; i < 100; i++) {
            new Thread(transferMoney).start();
            new Thread(transferMoney2).start();
        }


    }

    @Override
    public void run() {
        try {
            transfer(a, b, amount);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void transfer(Acount a, Acount b, int amount) throws InterruptedException {
        synchronized (a) {
            synchronized (b) {
                if (a.money < amount) {
                    System.out.println("Insufficient account balance, transfer failed");
                }
                a.money -= amount;
                b.money += amount;
                System.out.println("Transfer succeeded");
            }
        }
    }

    static class Acount {
        int money;

        public Acount(int money) {
            this.money = money;
        }
    }
}

Execution result:

Transfer 100 times to each other, and a total of 200 transfer results should be printed. But the program was only executed twice and did not go on. Let's analyze the causes of this problem.
For the transfer from Zhang San to Li Si, the two locks of Zhang San's account and Li Si's account must be obtained first to prevent other threads from operating the account during the transfer. If this happens: one thread holds Zhang San's account and the other thread holds Li Si's account, then two threads cannot request another account lock. The two threads fall into an infinite waiting state, and other threads that want to obtain the two account locks will wait all the time.

Make the order of obtaining locks consistent, and solve the problem of bank transfer

We can make the order of obtaining locks consistent to solve the above bank transfer problem. One of the reasons for the above problems is that different threads acquire locks in different order. When Zhang San transfers money to Li Si, first obtain Zhang San's account lock, and then obtain Li Si's account lock; When Li Si transfers money to Zhang San, first obtain Li Si's account lock, and then obtain Zhang San's account lock. In this way, a loop will be formed: thread 1 (Zhang San) - "thread 2 (Li Si) -" thread 1 (Zhang San). Therefore, as long as we destroy this loop, whether Zhang San transfers to Li Si or Li Si transfers to Zhang San, the order in which they obtain locks is the same, we can solve the deadlock problem. The following is the specific implementation.

/**
 * Change the order of obtaining locks to solve the problem of bank transfer deadlock.
 */
public class ChangeOrderDealTransferMoney implements Runnable {

    TransferMoney.Acount a;
    TransferMoney.Acount b;
    int amount;
    Object lock = new Object();

    public ChangeOrderDealTransferMoney(TransferMoney.Acount a, TransferMoney.Acount b, int amount) {
        this.a = a;
        this.b = b;
        this.amount = amount;
    }

    public static void main(String[] args) {
        TransferMoney.Acount acount = new TransferMoney.Acount(500);
        TransferMoney.Acount acount1 = new TransferMoney.Acount(500);
        ChangeOrderDealTransferMoney transferMoney = new ChangeOrderDealTransferMoney(acount, acount1, 200);
        ChangeOrderDealTransferMoney transferMoney2 = new ChangeOrderDealTransferMoney(acount1, acount, 200);
        for (int i = 0; i < 1000; i++) {
            new Thread(transferMoney).start();
            new Thread(transferMoney2).start();
        }
    }

    @Override
    public void run() {
        // Judge the order of obtaining locks according to the hash value. If there is a hash conflict, add a competing lock.
        if (a.hashCode() > b.hashCode()) {
            synchronized (a) {
                synchronized (b) {
                    transfer(a, b, amount);
                }
            }
        }
        if (a.hashCode() < b.hashCode()) {
            synchronized (b) {
                synchronized (a) {
                    transfer(a, b, amount);
                }
            }
        } else {
            synchronized (lock) {
                transfer(a, b, amount);
            }
        }
    }

    public static void transfer(TransferMoney.Acount a, TransferMoney.Acount b, int amount) {
        if (a.money < amount) {
            System.out.println("Insufficient account balance, transfer failed");
        }
        a.money -= amount;
        b.money += amount;
        System.out.println("Transfer succeeded");
    }
}

Execution result:

Judge the order of obtaining locks through the hash value of the account, so that the order of obtaining locks is the same whether Zhang San transfers to Li Si or Li Si transfers to Zhang San. If there is a hash conflict, a contention lock can be added to ensure thread safety. In actual production, the order of obtaining locks can be determined according to the index.

Philosophers' dining problems and corresponding solutions

There are five philosophers dining together. Each philosopher has only one knife and fork on the left and right sides. You can eat only when you pick up a knife and fork. Suppose every philosopher takes the tableware on his left hand first, then the tableware on his right hand, thinks after eating, and eats after thinking, what will happen. The following code is used to demonstrate:

/**
 * Philosopher dining problem demonstration.
 */
public class DiningPhilosopher implements Runnable {

    Object knife;
    Object cross;

    public DiningPhilosopher(Object knife, Object cross) {
        this.knife = knife;
        this.cross = cross;
    }

    public static void main(String[] args) {
        Object[] tableware = new Object[5];
        for (int i = 0; i < tableware.length; i++) {
            tableware[i] = new Object();
        }
        DiningPhilosopher[] diningPhilosophers = new DiningPhilosopher[5];
        for (int i = 0; i < diningPhilosophers.length; i++) {
            diningPhilosophers[i] = new DiningPhilosopher(tableware[i % 4], tableware[(i + 1) % 4]);
            new Thread(diningPhilosophers[i]).start();
        }
    }

    @Override
    public void run() {
        while (true) {
            doSomething("thinking");
            synchronized (knife) {
                synchronized (cross) {
                    doSomething("eating");
                    doSomething("put down cross");
                }
                doSomething("put down knife");
            }
        }
    }

    private void doSomething(String str) {
        System.out.println(Thread.currentThread().getName() + " " + str);
    }
}

Execution result:

You can see that the thread is stuck in a deadlock.

Thoughts on solving the dining problem of philosophers

There are many ways to solve the dining problem of philosophers, considering the four necessary conditions of breaking deadlock.
1: From the perspective of mutual exclusion conditions. Tableware can only be held by one person, so this condition cannot be destroyed.
2: Hold and request. Take the tableware first on the left hand and then on the right hand. You can ask the scientist to take the tableware on the left hand side and the tableware on the right hand side at the same time.
3: Inalienable. After getting the tableware, put it down only after eating by yourself. If there is a waiter, you can remind the philosopher to put down the tableware before eating, or you can solve this problem.
4: Loop wait. There are two ways to break the loop waiting. One is to change the order in which a philosopher takes tableware. Second: deal meal cards. Five people deal four meal cards. Only philosophers who get the meal cards can eat.
The above are some ideas to solve the dining problem.

Other thread activity issues

Livelock

Threads always do meaningless things

hunger

The thread has not been scheduled by the CPU