Avoid deadlock hazard

Posted by jamfrag on Fri, 18 Feb 2022 19:54:29 +0100

In the concurrent environment, in order to ensure the thread safety of sharing variable data, we need to use the locking mechanism. If the lock is not used properly, it may cause deadlock, thread starvation and other problems.

If a deadlock occurs in a Java application, the program cannot be recovered automatically, which will seriously cause the program to crash. Therefore, it is necessary to avoid the occurrence of deadlock in the design stage of development.

What is deadlock

Deadlock: each thread has the resources required by other threads and waits for the resources owned by other threads. Each thread will not give up the resources it already has until it obtains the required resources.

Scenario of program Deadlock:

1) Deadlock caused by cross lock

While thread A holds lock L and wants to obtain lock R, thread B holds lock R and tries to obtain lock L, then these two threads will block forever. Cross locks generally occur because threads acquire locks in different order.

2) Resource deadlock

When there is insufficient memory or we use thread pool and semaphore to limit resources in the program, the two threads wait for each other to release resources and enter permanent blocking.

3) Dead cycle deadlock

Due to the code defect or retry mechanism, the program makes the code fall into an endless loop, resulting in a large consumption of memory and cpu and blocking the thread.

When a deadlock occurs in a Java program, the blocked thread will never be used, and it may cause the program to stop or make the CPU soar, resulting in poor program performance. The only way to restore the program is to restart the application.

The occurrence of deadlocks is mostly accidental, which does not mean that a class has a deadlock. It has always been deadlocked, which is also the reason why it is difficult to investigate deadlocks.

Through the deadlock occurrence scenario, we can summarize the deadlock occurrence conditions:

  • Mutual exclusion: that is, the lock is exclusive, and only one thread can obtain the lock;

  • Occupy and wait: when the thread obtains the lock, if the required resources are not obtained, it will always block the resources needed to wait;

  • Non preemption: the resources held by the thread that obtains the lock cannot be preempted by other threads;

  • Circular waiting: threads that fall into deadlock waiting must form a circular waiting loop.

Deadlock Detection

If a program obtains at most one lock at a time, the deadlock problem will not occur. However, there are often scenarios in which the program needs to obtain multiple locks, so the order of locks must be considered at this time.

If all threads acquire locks in a fixed order, there will be no deadlock problem. When threads try to acquire locks in different order, deadlock will occur.

The following example will Deadlock:

public class DeadlockTest {
    //Create two lock objects
    private final Object leftMonitor = new Object();
    private final Object rightMonitor = new Object();
    /**
     * Hold the L lock and want to obtain the R lock
     */
    @SneakyThrows
    public void leftForRight() {
        synchronized (leftMonitor){
            //Sleep and give R a chance to lock
            TimeUnit.SECONDS.sleep(1);
            synchronized (rightMonitor){
                System.out.println("leftForRight Get lock");
            }
        }
    }

    /**
     * Hold the R lock and obtain the L lock
     */
    public void rightForLeft() {
        synchronized (rightMonitor){
            synchronized (leftMonitor){
                System.out.println("rightForLeft Get lock");
            }
        }
    }

    public static void main(String[] args) {
        DeadlockTest deadlockTest = new DeadlockTest();
        ExecutorService executor = Executors.newFixedThreadPool(2);
        executor.execute(()->{
            deadlockTest.leftForRight();
        });
        executor.execute(()->{
            deadlockTest.rightForLeft();
        });
        executor.shutdown();

    }

}

We can view deadlock information through jstack or jconsole tools provided by JDK.

jstack -l pid view stack information:

Or jconsole connects to the process:

Deadlock information can be seen directly through stack information.

We'll talk about how to dump stack information in linux environment later.

Deadlock avoidance

We can avoid deadlock by breaking the condition of deadlock occurrence.

The business in the program requires us to use exclusive locks instead of shared locks, so we can't break the mutex of locks.

Destroy possession and wait: apply for all resources at one time;

Destruction cannot be preempted: use the tryLock function in the display Lock to replace the built-in Lock synchronized, which can detect deadlocks and recover from deadlocks. Threads that use built-in locks will be blocked if they fail to obtain the Lock, while explicit locks can specify a Timeout. After waiting for the set time, tryLock will return a failure message and release its own resources.

Breaking cycle waiting: make threads acquire locks in a fixed order. In the design, we should minimize the number of lock interactions, design the lock order in advance and strictly abide by it.

Concluding remarks

This is the end of learning the basic knowledge of concurrent programming. If you encounter relevant knowledge in the future, you can supplement it.

The next series of "Java Basics" set sail, and the knowledge of class loading and data structure (including thread safe data structure) will meet you.

Topics: Java Multithreading Concurrent Programming