Problems and solutions encountered in the use of polling lock!

When we encounter a deadlock, we can not only manually restart the program to solve it, but also consider using sequence lock and polling lock. For this part, please refer to my previous article, which will not be repeated here. However, improper use of polling lock will bring new serious problems, so in this article, we will learn about these problems and corresponding solutions.

Problem demonstration

Before we use the polling lock, the following problems may occur:

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

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

        // Create thread 1
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                lockA.lock(); // Lock
                System.out.println("Thread 1:Get lock A!");
                try {
                    Thread.sleep(1000);
                    System.out.println("Thread 1:Waiting for acquisition B...");
                    lockB.lock(); // Lock
                    try {
                        System.out.println("Thread 1:Get lock B!");
                    } finally {
                        lockA.unlock(); // Release lock
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    lockA.unlock(); // Release lock
                }
            }
        });
        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
    }
}

The execution result of the above code is 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.

Simple version polling lock

When a deadlock problem occurs, we can use polling lock to solve it. Its implementation idea is to obtain multiple locks by polling. If any lock fails to be obtained in the middle of the process, we will perform a fallback operation to release all locks owned by the current thread and wait for the next re execution. In this way, we can avoid multiple threads owning and occupying lock resources at the same time, Thus, the deadlock problem is directly solved. The simple version of polling lock is implemented as follows:

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

public class SolveDeadLockExample2 {
    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
     */
    private static void pollingLock(Lock lockA, Lock lockB) {
        // Polling lock
        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 execution result of the above code is as follows:

From the above results, we can see that when we use the polling lock in the program, there will be no deadlock problem, but the above polling lock is not perfect. Let's take a look at the problems of this polling lock?

Question 1: dead cycle

For the above simple version of the polling lock, if a thread has been occupying the lock resources or occupying the lock resources for a long time, it will lead to the polling lock entering an endless loop state. It will try to obtain the lock resources all the time, which will cause new problems and unnecessary performance overhead. The specific examples are as follows.

Counterexample

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 {
                    // If the code here is not executed, thread 2 has not released the lock resource
                    // lockB.unlock(); 
                }
            }
        });
        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 execution result of the above code is as follows:

Thread 1 polling lock has entered an endless loop state.

Optimized version

In view of the above dead cycle, we can improve the following two ideas:

  1. Add maximum times limit: if n
    If the lock has not been acquired after the first attempt to acquire the lock, it is considered to have failed to acquire the lock, and the polling is terminated after the failure policy is executed (the failure policy can be logging or other operations);
  2. Add the maximum time limit: if the lock is not acquired after n seconds of trying to acquire the lock, it is considered to have failed to acquire the lock, and the polling is terminated after the failure policy is executed.

Any of the above strategies can solve the problem of dead cycle. Considering the implementation cost, we can improve the polling lock by polling the maximum number of times. 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, 3);
            }
        });
        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 {
                    // Thread 2 forgot to release the lock resource
                    // lockB.unlock(); //  Release lock
                }
            }
        });
        t2.start(); // Run thread
    }

    /**
     * Polling lock
     *
     * maxCount: Maximum number of polls
     */
    public static void pollingLock(Lock lockA, Lock lockB, int maxCount) {
        // Polling count
        int count = 0;
        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.");
                }
            }

            // Determine whether the maximum number of times has been exceeded
            if (count++ > maxCount) {
                // Terminate cycle
                System.out.println("Polling lock acquisition failed,Log or execute other failure policies");
                return;
            }

            // Wait one second before attempting to acquire the lock
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

The execution result of the above code is as follows:

From the above results, we can see that after we improve, the polling lock will not have the problem of dead loop. It will try a certain number of times and terminate the execution.

Question 2: threads starve to death

The polling waiting time of the above polling lock is fixed, as shown in the following code:

// Wait 1 s before attempting to acquire (poll) the lock
try {
    Thread.sleep(1000);
} catch (InterruptedException e) {
    e.printStackTrace();
}

This will cause the thread to starve under special circumstances, that is, the polling lock cannot obtain the lock all the time. For example, the following example.

Counterexample

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, 3);
            }
        });
        t1.start(); // Run thread

        // Create thread 2
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    lockB.lock(); // Lock
                    System.out.println("Thread 2:Get lock B!");
                    try {
                        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
                        }
                    } finally {
                        lockB.unlock(); // Release lock
                    }
                    // Wait one second before proceeding
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        });
        t2.start(); // Run thread
    }

    /**
     * Polling lock
     */
    public static void pollingLock(Lock lockA, Lock lockB, int maxCount) {
        // Cycle counter
        int count = 0;
        while (true) {
            if (lockA.tryLock()) { // Attempt to acquire lock
                System.out.println("Thread 1:Get lock A!");
                try {
                    Thread.sleep(100); // Wait 0.1s (time required to acquire lock)
                    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.");
                }
            }

            // Determine whether the maximum number of times has been exceeded
            if (count++ > maxCount) {
                // Terminate cycle
                System.out.println("Polling lock acquisition failed,Log or execute other failure policies");
                return;
            }

            // Wait one second before attempting to acquire the lock
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

The execution result of the above code is as follows:

As can be seen from the above results, Thread 1 (polling lock) has failed to acquire the lock. The reason for this result is that thread 1 waits for a fixed 1s for each polling, and thread 2 acquires the lock every 1s. This will cause thread 2 to successfully acquire the lock first, while thread 1 will always starve to death. The execution process is shown in the following figure:

Optimized version
Next, we can improve the fixed waiting time of the polling lock to the mode of fixed time + random time, so as to avoid the problem of "starvation" of the polling lock due to the same frequency of obtaining the lock. The specific implementation code is as follows:

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

public class SolveDeadLockExample {
    private static Random rdm = new Random();

    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, 3);
            }
        });
        t1.start(); // Run thread

        // Create thread 2
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    lockB.lock(); // Lock
                    System.out.println("Thread 2:Get lock B!");
                    try {
                        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
                        }
                    } finally {
                        lockB.unlock(); // Release lock
                    }
                    // Wait one second before proceeding
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        });
        t2.start(); // Run thread
    }

    /**
     * Polling lock
     */
    public static void pollingLock(Lock lockA, Lock lockB, int maxCount) {
        // Cycle counter
        int count = 0;
        while (true) {
            if (lockA.tryLock()) { // Attempt to acquire lock
                System.out.println("Thread 1:Get lock A!");
                try {
                    Thread.sleep(100); // Wait 0.1s (time required to acquire lock)
                    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.");
                }
            }

            // Determine whether the maximum number of times has been exceeded
            if (count++ > maxCount) {
                // Terminate cycle
                System.out.println("Polling lock acquisition failed,Log or execute other failure policies");
                return;
            }

            // Wait for a certain time (fixed time + random time) before continuing to try to obtain the lock
            try {
                Thread.sleep(300 + rdm.nextInt(8) * 100); // Fixed time + random time
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

The execution result of the above code is as follows:

From the above results, it can be seen that thread 1 (polling lock) will not starve after adding the random waiting time.
summary

In this paper, we introduce the use of polling lock to solve the deadlock problem, but the simple version of polling lock will cause the problems of dead loop and thread starvation in some cases. Therefore, we optimize the polling lock, add the maximum polling times and random round robin waiting time to the polling lock, so as to solve the new problems caused by the introduction of polling lock, In this way, it can be used happily to solve the deadlock problem.

Reference & acknowledgment
Java Concurrent Programming Practice

Author: Java Chinese community link: https://juejin.cn/post/7002391952876372004 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.

Keywords: Java Go Database

Added by SwiftlyTilting on Sat, 18 Dec 2021 03:23:42 +0200