What about Java Concurrent Programming Actual 04 Deadlock?

Series of Java Concurrent Programming Articles

Java Concurrent Programming Actual 01 Bug Source for Concurrent Programming
Java Concurrent Programming Practice 02 How Java Solves Visibility and Orderliness Problems
Java Concurrent Programming Actual 03 Mutual Exclusion Lock Solves Atomic Problems

premise

In the final example of the third article, you need to acquire locks on two accounts to transfer money, which can lead to deadlocks. I'll put the code snippet from the previous chapter below:

public class Account {
    // balance
    private Long money;
    public synchronized void transfer(Account target, Long money) {
        synchronized(this) {           (1)
            synchronized (target) {    (2)
                this.money -= money;
                if (this.money < 0) {
                    // throw exception
                }
                target.money += money;
            }
        }
    }
}

If account A is transferred to account B100, account B is also transferred to account A100. When thread A of account A transfers to code (1), the lock of account A object is acquired, and thread B of account B transfers to code (1), the lock of account B object is acquired.When thread A and thread B reach code (2), they both wait for each other to release the lock to acquire it, but synchronized is a blocking lock, and the lock will not be released until the code block has been executed. Thread A and thread B are dead against each other, and no one can let go.Wait until the day you restart the app.This phenomenon is deadlock.
Deadlock is defined as the phenomenon of a group of competing threads waiting for each other to become "permanently" blocked.
As follows:

Find Deadlock Information

Let me take a program that is basically deadlocked as an example, and create two threads. Thread A acquires lock A and then hibernates for one second to acquire lock B. Thread B acquires lock B and then hibernates for one second to acquire lock A.Then basically deadlocks will occur, and the code is as follows:

public class DeadLock extends Thread {
    private String first;
    private String second;
    public DeadLock(String name, String first, String second) {
        super(name); // Thread name
        this.first = first;
        this.second = second;
    }

    public  void run() {
        synchronized (first) {
            System.out.println(this.getName() + " Acquire locks: " + first);
            try {
                Thread.sleep(1000L); //Thread hibernates for 1 second
                synchronized (second) {
                    System.out.println(this.getName() + " Acquire locks: " + second);
                }
            } catch (InterruptedException e) {
                // Do nothing
            }
        }
    }
    public static void main(String[] args) throws InterruptedException {
        String lockA = "lockA";
        String lockB = "lockB";
        DeadLock threadA = new DeadLock("ThreadA", lockA, lockB);
        DeadLock threadB = new DeadLock("ThreadB", lockB, lockA);
        threadA.start();
        threadB.start();
        threadA.join(); //Waiting for thread 1 to finish executing
        threadB.join();
    }
}

A deadlock occurs when the program is run, and then the JPS command (jps.exe is in the jdk/bin directory) is used, as follows:

C:\Program Files\Java\jdk1.8.0_221\bin>jps -l
24416 sun.tools.jps.Jps
24480 org.jetbrains.kotlin.daemon.KotlinCompileDaemon
1624
20360 org.jetbrains.jps.cmdline.Launcher
9256
9320 page2.DeadLock
18188

You can discover the process id 9320 where the deadlock occurred, and then use the jstack (jstack.exe in the jdk/bin directory) command to view the deadlock information.

C:\Program Files\Java\jdk1.8.0_221\bin>jstack 9320
"ThreadB" #13 prio=5 os_prio=0 tid=0x000000001e48c800 nid=0x51f8 waiting for monitor entry [0x000000001f38f000]
   java.lang.Thread.State: BLOCKED (on object monitor)
        at page2.DeadLock.run(DeadLock.java:19)
        - waiting to lock <0x000000076b99c198> (a java.lang.String)
        - locked <0x000000076b99c1d0> (a java.lang.String)

"ThreadA" #12 prio=5 os_prio=0 tid=0x000000001e48c000 nid=0x3358 waiting for monitor entry [0x000000001f28f000]
   java.lang.Thread.State: BLOCKED (on object monitor)
        at page2.DeadLock.run(DeadLock.java:19)
        - waiting to lock <0x000000076b99c1d0> (a java.lang.String)
        - locked <0x000000076b99c198> (a java.lang.String)

This allows us to see the information that a deadlock has occurred.Deadlock was found, but the only way to resolve it is to restart the application.

How to avoid deadlocks

1. Fixed order to acquire locks

If all threads acquire locks in a fixed order, then lock order deadlocks will not occur in the program.(From Java Concurrent Programming Practice)
There are many ways to verify the consistency of lock order. If the locked object contains an incremental id field (unique, immutable, comparable), it is much easier. The order in which locks are acquired is sorted from small to large.Or use an example of a transfer, coded as follows:

public class Account {
    // id (incremental)
    private Integer id;
    // balance
    private Long money;
    public synchronized void transfer(Account target, Long money) {
        Account account1;
        Account account2;
        if (this.id < target.id) {
            account1 = this;
            account2 = target;
        } else {
            account1 = target;
            account2 = this;
        }

        synchronized(account1) {
            synchronized (account2) {
                this.money -= money;
                if (this.money < 0) {
                    // throw exception
                }
                target.money += money;
            }
        }
    }
}

If the object does not have a unique, immutable, comparable field (such as an incremental id), the hash value returned by the System.identityHashCode() method can be used for comparison.The comparison can be similar to the example above.System.identityHashCode() has a very low probability of hash conflicts, although hash conflicts occur.So this technology gives you the most security at the least cost.
Tip: The System.identityHashCode() method only returns the default hash value, regardless of whether you override the object's hashCode method or not.

2. Apply for all resources at once

As long as the resource locks to both outgoing and inbound accounts are acquired.When the transfer operation is completed, the resource locks for both the transfer account and the transfer account are also released.Then no deadlock will occur.However, synchronized locks can only lock one resource lock at a time, so a lock allocator, LockAllocator, needs to be established.The code is as follows:

/** Lock allocator (singleton class) */
public class LockAllocator {
    private final List<Object> lock = new ArrayList<Object>();
    /** Also apply for lock resources */
    public synchronized boolean lock(Object object1, Object object2) {
        if (lock.contains(object1) || lock.contains(object2)) {
            return false;
        }

        lock.add(object1);
        lock.add(object2);
        return true;
    }
    /** Release resource locks at the same time */
    public synchronized void unlock(Object object1, Object object2) {
        lock.remove(object1);
        lock.remove(object2);
    }
}

public class Account {
    // balance
    private Long money;
    // Lock allocator
    private LockAllocator lockAllocator;
    
    public void transfer(Account target, Long money) {
        try {
            // Loop to acquire locks until successful
            while (!lockAllocator.lock(this, target)) {
            }

            synchronized (this){
                synchronized (target){
                    this.money -= money;
                    if (this.money < 0) {
                        // throw exception
                    }
                    target.money += money;
                }
            }
        } finally {
            // Release lock
            lockAllocator.unlock(this, target);
        }
    }
}

Use while loops to keep acquiring locks until you succeed, but you can also set up an xx millisecond hibernation after a failure, or other optimized method.To release a lock, you must use try-finally to release the lock.Avoid failure to release lock.

3. Attempt to acquire lock resources

In Java, the Lock interface defines an abstract set of locking operations.Unlike built-in lock synchronized, when a built-in lock is used, it will die as long as it is not acquired. Display Lock provides an unconditional, pollable, timed, and interruptible lock acquisition operation. All lock and unlock operations are displayed (both lock and unlock operations with built-in lock synchronized are implied). This article will not expand onLock is displayed (of course, friends who are interested can Baidu first).

summary

Deadlock can be a serious problem in production environments. Although restarting an application to resolve the deadlock is a very costly production environment, and deadlock may occur after restarting an application. Therefore, very rigorous measures are needed to avoid deadlock when writing concurrent programs.There should be more schemes to avoid deadlocks, despicable talent, for now.If there are other options, you can leave a message to inform.Thank you very much for your reading. Thank you.

Reference article: Java Concurrent Programming Practice Chapter 10 Geek Time: Java Concurrent Programming Actual 05: Incidentally deadlocked, what to do? Geek Time: Java Core Technology Interview Lecture 18: When do Java programs deadlock?How to locate, repair?

Personal Blog URL: https://colablog.cn/

If my article helps you, you can follow my WeChat Public Number and share it with you for the first time

Keywords: Programming Java JDK

Added by Thikho on Tue, 12 May 2020 06:08:58 +0300