Deadlock Terminator: sequence lock and polling lock!

Posted by wvwisokee on Thu, 23 Dec 2021 12:12:23 +0100

Dead Lock refers to two or more computing units (processes, threads or coroutines) that are waiting for the other party to stop execution to obtain system resources, but none of them exits in advance.

The deadlock example code is as follows:

public class DeadLockExample {
    public static void main(String[] args) {
        Object lockA = new Object(); // Create lock A
        Object lockB = new Object(); // Create lock B

        // Create thread 1
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (lockA) {
                    System.out.println("Thread 1:Get lock A!");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("Thread 1:Waiting for acquisition B...");
                    synchronized (lockB) {
                        System.out.println("Thread 1:Get lock B!");
                    }
                }
            }
        });
        t1.start(); // Run thread

        // Create thread 2
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (lockB) {
                    System.out.println("Thread 2:Get lock B!");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("Thread 2:Waiting for acquisition A...");
                    synchronized (lockA) {
                        System.out.println("Thread 2:Get lock A!");
                    }
                }
            }
        });
        t2.start(); // Run thread
    }
}

The results of the above procedures are as follows:

It can be seen from the above results that threads wait for each other and try to obtain each other's (lock) resources in the program, which is a typical deadlock problem.

From the above example analysis, it can be concluded that the following four conditions need to be met to generate Deadlock:

Mutually exclusive condition: it means that the operation unit (process, thread or co process) is exclusive to the allocated resources, that is, a lock resource can only be occupied by one operation unit within a period of time.
Request and hold condition: it means that the computing unit has held at least one resource, but has made a new resource request, and the resource has been occupied by other computing units. At this time, the requesting computing unit is blocked, but it still holds the other resources it has obtained.
Inalienable condition: it means that the resources obtained by the computing unit cannot be deprived before they are used up.
Loop waiting condition: when a deadlock occurs, there must be a loop chain of computing units and resources, that is, the computing unit is waiting for the resources occupied by another computing unit, and the other party is waiting for the resources occupied by itself, resulting in loop waiting.

Only when these four conditions are met at the same time can deadlock be caused. ​
In other words, to generate a deadlock, we must meet the above four conditions at the same time. Then we can solve the deadlock problem by breaking any condition.

Deadlock solution analysis

Next, let's analyze the four conditions for deadlock, which can be broken? What cannot be destroyed?

  • Mutex condition: a system characteristic that cannot be destroyed.
  • Request and hold conditions: can be destroyed.
  • Inalienable conditions: system characteristics, which cannot be destroyed.
  • Loop wait condition: can be destroyed.
    Through the above analysis, we can draw a conclusion that we can only solve the deadlock problem by breaking the request and hold conditions or loop waiting conditions. Then, when we go online, we will first solve the deadlock problem by breaking the "loop waiting conditions".

Solution 1: sequence lock

The so-called sequential lock means that the deadlock problem is solved by obtaining the lock in order to avoid the loop waiting condition. ​

When we do not use sequence lock, the execution of the program may be as follows:

Thread 1 obtains lock a first and then lock B. thread 2 executes simultaneously with thread 1. Thread 2 obtains lock B first and then lock A. in this way, both sides occupy their own resources (lock a and lock b) before trying to obtain each other's lock, resulting in loop waiting and deadlock.
At this time, we only need to unify the order in which thread 1 and thread 2 acquire locks, that is, after thread 1 and thread 2 execute at the same time, they acquire lock A first, and then acquire lock B. the execution process is shown in the following figure:

Because only one thread can successfully obtain lock A, the thread that does not obtain lock A will wait to obtain lock A first. At this time, the thread that obtains lock A will continue to obtain lock B. because no thread competes for and owns lock B, the thread that obtains lock A will successfully own lock B, and then execute the corresponding code to release all lock resources, Then another thread waiting to obtain lock A can successfully obtain the lock resource and execute the subsequent code, so that there will be no deadlock.
The implementation code of sequence lock is as follows:

public class SolveDeadLockExample {
    public static void main(String[] args) {
        Object lockA = new Object(); // Create lock A
        Object lockB = new Object(); // Create lock B
        // Create thread 1
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (lockA) {
                    System.out.println("Thread 1:Get lock A!");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("Thread 1:Waiting for acquisition B...");
                    synchronized (lockB) {
                        System.out.println("Thread 1:Get lock B!");
                    }
                }
            }
        });
        t1.start(); // Run thread
        // Create thread 2
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (lockA) {
                    System.out.println("Thread 2:Get lock A!");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("Thread 2:Waiting for acquisition B...");
                    synchronized (lockB) {
                        System.out.println("Thread 2:Get lock B!");
                    }
                }
            }
        });
        t2.start(); // Run thread
    }
}

The results of the above procedures are as follows:

From the above execution results, it can be seen that the program does not have the problem of deadlock.

Solution 2: polling lock

Polling lock avoids deadlock by breaking the "request and hold conditions". Its implementation idea is simply to try to obtain the lock through polling. If one lock fails to be obtained, release all locks owned by the current thread and wait for the next round to try to obtain the lock.

The implementation of polling lock requires the tryLock method of ReentrantLock. The specific implementation code is as follows:

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class SolveDeadLockExample {
    
    public static void main(String[] args) {
        Lock lockA = new ReentrantLock(); // Create lock A
        Lock lockB = new ReentrantLock(); // Create lock B

        // Create thread 1 (using polling lock)
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                // Call polling lock
                pollingLock(lockA, lockB);
            }
        });
        t1.start(); // Run thread

        // Create thread 2
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                lockB.lock(); // Lock
                System.out.println("Thread 2:Get lock B!");
                try {
                    Thread.sleep(1000);
                    System.out.println("Thread 2:Waiting for acquisition A...");
                    lockA.lock(); // Lock
                    try {
                        System.out.println("Thread 2:Get lock A!");
                    } finally {
                        lockA.unlock(); // Release lock
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    lockB.unlock(); // Release lock
                }
            }
        });
        t2.start(); // Run thread
    }
    
     /**
     * Polling lock
     */
    public static void pollingLock(Lock lockA, Lock lockB) {
        while (true) {
            if (lockA.tryLock()) { // Attempt to acquire lock
                System.out.println("Thread 1:Get lock A!");
                try {
                    Thread.sleep(1000);
                    System.out.println("Thread 1:Waiting for acquisition B...");
                    if (lockB.tryLock()) { // Attempt to acquire lock
                        try {
                            System.out.println("Thread 1:Get lock B!");
                        } finally {
                            lockB.unlock(); // Release lock
                            System.out.println("Thread 1:Release lock B.");
                            break;
                        }
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    lockA.unlock(); // Release lock
                    System.out.println("Thread 1:Release lock A.");
                }
            }
            // Wait one second before proceeding
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

The results of the above procedures are as follows:

It can be seen from the above results that the above code does not have the problem of deadlock
summary
This paper introduces two solutions to deadlock:

  • The first kind of sequential lock: by changing the order of obtaining the lock, the "loop request condition" is broken to avoid the occurrence of deadlock;
  • The second kind of polling lock: the deadlock problem is solved by polling, that is, breaking the "request and ownership conditions". Its implementation idea is to try to obtain the lock by spinning. If any lock fails to be obtained on the way to obtain the lock, release all the previously obtained locks and wait for a period of time to execute the previous process again, so as to avoid the embarrassment that a lock has been occupied (by a thread), so as to avoid the deadlock problem.

Author: Java Chinese community link: https://juejin.cn/post/7001293223997997064 Source: Nuggets
The copyright belongs to the author. For commercial reprint, please contact the author for authorization, and for non-commercial reprint, please indicate the source.

Topics: Java Go Database