What is a deadlock?

Definition: Two or more threads are said to be deadlocked if they are permanently suspended because they are WAITING for each other (their lifecycle status is BLOCKED or WAITING).

Because the life cycle state of the thread that produced the deadlock is never runnable, the task that these threads ask to execute never progresses.

In layman’s terms, when thread A holds the exclusive lock A and tries to acquire the exclusive lock B, thread B holds the exclusive lock B, And try to obtain the exclusive lock A, and the premise of a and B to release the lock they hold is to obtain another lock held by the other party, so the two threads can not obtain the other lock they apply for, and finally the two threads are in the infinite waiting state, that is, the deadlock occurs.

The necessary conditions for a deadlock to occur

There are four necessary conditions for a deadlock:

(1) Mutual exclusion of resources: The resources involved must be exclusive, that is, each resource can only be used by one thread at a time. If another thread accesses the resource, it can only wait until the thread that occupies the resource is finished using it and releases the resource.

(2) Occupying and waiting for resources: after the thread has obtained a certain resource, it sends a request to other resources, but the resource may be occupied by other threads. At this time, the request is blocked, but the resource it has obtained remains unreleased;

(3) Resource inalienable: the resource that has been acquired by the thread can not be deprived before it is used and can only be released after it is used.

(4) Circular waiting for resources: the threads involved must be waiting for resources held by other threads, and these threads in turn are waiting for resources held by the first thread, that is, several threads form a head to tail circular waiting resource relationship.

These conditions are necessary but not sufficient conditions for the occurrence of a deadlock, which means that if a deadlock occurs, the above conditions must be true at the same time, but even if the above conditions are true at the same time, they do not necessarily result in the occurrence of a deadlock.

How to avoid and handle deadlocks?

Basic methods for handling deadlocks:

We only have to break one of the four conditions that cause the deadlock.

Because locks are exclusive and can only be held by the thread that owns them, deadlock caused by locks can only be eliminated in the two directions of “holding and waiting for resources” and “cyclic waiting for resources”.

Method one: rough lock method

Using coarse-grained locks instead of multiple locks eliminates deadlocks by requiring only one lock for each thread involved.

The disadvantage of rough locking is that it significantly reduces concurrency and can lead to resource waste.

Method two: lock sort method

Related threads apply for locks in a globally uniform order. Assuming that multiple threads need to apply for resources (locks), we can eliminate the condition of “waiting for resources in a loop” and avoid deadlocks by simply making them apply for locks in a globally uniform order.

Method 3: Use reentrantLock. tryLock() to request a lock

The tryLock method allows us to specify a timeout for the lock request operation. During the timeout period, the method returns true if the lock was successfully applied for. If the lock is held by another thread during execution, the method suspends the thread until the lock is successfully applied for or until the specified timeout period is exceeded (the method returns false).

Using this method to apply for a lock prevents one thread from waiting indefinitely for a resource held by another thread, thus ultimately eliminating the “hold and wait for resource” requirement for deadlock.

Deadlock example

Deadlock instances

public class DeadLockDemo { private static Object resource1 = new Object(); Private static Object Resource2 = new Object(); private static Object resource2 = new Object(); Public static void main(String[] args) {new Thread(() -> {synchronized (resource1) { System.out.println(Thread.currentThread() +"get resource1");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread() + "waiting get resource2");
                synchronized (resource2) {
                    System.out.println(Thread.currentThread() + "get resource2"); }}},Thread 1 "").start();

        new Thread(() -> {
            synchronized (resource2) {
                System.out.println(Thread.currentThread() + "get resource2");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread() + "waiting get resource1");
                synchronized (resource1) {
                    System.out.println(Thread.currentThread() + "get resource1"); }}},Thread 2 "").start(); Thread[Thread 1,5,main]get resource1 Thread[Thread 2,5,main]get resource2 Thread[Thread 1,5,main]waiting get resource2 Thread[Thread 2,5,main]waiting get resource1Copy the code

Thread A acquires the monitor lock of resource1 through synchronized (resource1) and thread.sleep (1000); Let thread A sleep for 1s so that thread B can execute and then acquire the monitor lock of Resource2. When thread A and thread B sleep, they both attempt to request resources from each other, and then the two threads fall into A state of waiting for each other, resulting in A deadlock. The above example meets the four necessary conditions for a deadlock to occur.

We changed the code for thread 2 to the following so that deadlocks would not occur. new Thread(() -> { synchronized (resource1) { System.out.println(Thread.currentThread() +"get resource1");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread() + "waiting get resource2");
                synchronized (resource2) {
                    System.out.println(Thread.currentThread() + "get resource2"); }}},Thread 2 "").start(); Thread[Thread 1,5,main]get resource1 Thread[Thread 1,5,main]waiting get resource2 Thread[Thread 1,5,main]get resource2 Thread[Thread 1,5,main 2,5,main]get resource1 Thread[Thread 2,5,main]waiting get resource2 Thread[Thread 2,5,main]get resource2 Process finished withexit code 0
Copy the code

Thread 1 is the first to acquire the monitor lock of Resource1, at which point thread 2 cannot acquire it. Thread 1 then acquires the monitor lock for Resource2, which is available. Thread 1 then releases the monitor lock on resource1 and resource2, which thread 2 acquires and executes. This breaks the break loop wait condition, thus avoiding deadlocks.

What is a live lock

Live lock: An active failure in which a thread is always running but its task never progresses.

The difference between a live lock and a deadlock is that an entity in a live lock is in a state of constant change, while an entity in a deadlock is waiting. Live locks can unlock themselves, deadlocks cannot.

What is thread hunger

Hunger: A state in which one or more threads are unable to execute because they cannot get the resources they need for any reason.

Causes of hunger in Java:

  1. High-priority threads eat up the CPU time of all low-priority threads.
  2. A thread is permanently blocked in a state waiting to enter a synchronized block because other threads continue to access the synchronized block before it does, such as using an unfair lock.
  3. A thread is waiting for an object that is itself in permanent wait completion (such as calling its wait method) because other threads are constantly being woken up.

What is unlocked

All threads can access and modify the same resource, but only one thread can modify the resource successfully. If multiple threads modify the same value, one thread will be able to successfully modify the value, while other threads that fail to modify the value will retry until the value is successfully modified without locking (such as JDK CAS).