java Concurrent Programming -- the practice of fair and unfair locking

Posted by girishn on Sun, 09 Jan 2022 07:03:58 +0100

preface

stay java Concurrent Programming (16) -- seven categories and characteristics of locks In this paper, we classify locks into various dimensions, one of which is fair / unfair. In this paper, we discuss fair and unfair locks.

Fair | unfair

First, let's look at what are fair locks and unfair locks. Fair locks refer to allocating locks according to the order of thread requests; Non fair lock means that queue jumping can be allowed under certain circumstances, which is not completely in the order of requests. However, it should be noted that the unfairness here does not mean completely random. It does not mean that threads can jump in the queue at will, but only "at the right time".

So when is the right time? Suppose that when the current thread requests to obtain the lock, the previous thread holding the lock happens to release the lock, then the thread applying for the lock can choose to jump the queue immediately regardless of the waiting thread. However, if the previous thread does not release the lock at that time when the current thread requests, the current thread will still enter the waiting queue.

In order to better understand fair locks and unfair locks, let's take an example in life. Suppose we are still studying at school and queuing up to buy food in the canteen. I am the second in the queue. There is a classmate in front of me, but at this time, I don't think about lunch, but a math problem in the morning and fall into deep thought, So when it was my turn after the students in front of me finished dinner, I was distracted and didn't notice that it was my turn now. At this time, the students in front suddenly came back to jump in the queue and said, "sorry, aunt, please add a chicken leg to me". Such behavior can be compared with our fair lock and unfair lock.

Seeing this, you may wonder why we should set the unfair policy, and the unfair policy is still the default policy of ReentrantLock. If we don't set it, the default is unfair. Is my queuing time wasted? Why do others have priority over me? After all, fairness is a good behavior, not fairness is a bad behavior.

Let's consider A case. Suppose thread A holds A lock and thread B requests the lock. Since thread A already holds the lock, thread B will fall into waiting. When waiting, thread B will be suspended, that is, it will enter the blocking state. When thread A releases the lock, it should be thread B's turn to wake up and obtain the lock, However, if A thread C suddenly cuts in the queue to request this lock, the lock will be given to thread C according to the unfair strategy. This is because waking up thread B requires A lot of overhead. It is likely that thread C has obtained the lock and released the lock after executing the task before waking up. Compared with the long process of waiting for thread B to wake up, jumping in the queue will make thread C skip the blocking process. If there is not much to be executed in the lock code, thread C can quickly complete the task and hand over the lock before thread B is fully awakened. This is A win-win situation for thread C, There is no need to wait, which improves its efficiency. For thread B, the time for it to obtain the lock is not delayed, because thread C will release the lock long after it is awakened, because the execution speed of thread C is very fast compared with the wake-up speed of thread B. therefore, Java designers design unfair locks to improve the overall operation efficiency.

Usage scenario

Fair scene

Let's illustrate the fair and unfair scenarios with diagrams. Let's look at the fair situation first. Suppose we create a fair lock. At this time, four threads request the fair lock in sequence. After thread 1 obtains the lock, threads 2, 3 and 4 will start waiting in the waiting queue. Then, after thread 1 releases the lock, threads 2, 3 and 4 will obtain the lock in turn. The reason why thread 2 obtains it first is that it waits the longest.

An unfair scene

Let's take another look at the unfair situation. Assuming that thread 5 suddenly attempts to obtain the lock when thread 1 is unlocking, thread 5 can get the lock according to our unfair strategy. Although it does not enter the waiting queue, and threads 2, 3 and 4 wait longer than thread 5, considering the overall efficiency, The lock will still be held by thread 5.

Code practice

Code demonstration

package com.concurrency;

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

/**
 * Demonstrate fair locks, showing fair and unfair situations respectively. An unfair lock will give priority to the thread holding the lock to obtain the lock again.
 */
public class FairAndUnfair {


    public static void main(String[] args) {
        PrintQueue printQueue=new PrintQueue();
        Thread thread[]=new Thread[10];
        for (int i = 0; i < 10; i++) {
            thread[i]=new Thread(new Job(printQueue),"thread "+i);
        }

        for (int i = 0; i < 10; i++) {
            thread[i].start();
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

    }

    static class Job implements Runnable{

        private PrintQueue printQueue;
        public Job(PrintQueue printQueue){
            this.printQueue=printQueue;
        }

        @Override
        public void run() {
            System.out.printf("%s :going to print a job\n",Thread.currentThread().getName());
            printQueue.printJob(new Object());
            System.out.printf("%s :the document has been printed\n",Thread.currentThread().getName());

        }
    }



    static class PrintQueue{

        //Set to unfair. false means unfair, and true means fair
        private final Lock queueLock=new ReentrantLock(false);

        public void printJob(Object document){
            queueLock.lock();
            try {
            long duration=(long)(Math.random()*10000);
            System.out.printf(" %s:PrintQueue: print a job during %d seconds\n",
                    Thread.currentThread().getName(),(duration / 1000));

                Thread.sleep(duration);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }finally {
                queueLock.unlock();
            }


            queueLock.lock();
            try {
            long duration=(long)(Math.random()*10000);
            System.out.printf(" %s:PrintQueue: print a job during %d seconds\n",
                    Thread.currentThread().getName(),(duration / 1000));

                Thread.sleep(duration);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }finally {
                queueLock.unlock();
            }



        }
    }
}

We can set fair / unfair locks by changing the parameters in new ReentrantLock(false).

Operating results under fair conditions:

thread 0 :going to print a job
 thread 0:PrintQueue: print a job during 4 seconds
thread 1 :going to print a job
thread 2 :going to print a job
thread 3 :going to print a job
thread 4 :going to print a job
thread 5 :going to print a job
thread 6 :going to print a job
thread 7 :going to print a job
thread 8 :going to print a job
thread 9 :going to print a job
 thread 1:PrintQueue: print a job during 7 seconds
 thread 2:PrintQueue: print a job during 8 seconds
 thread 3:PrintQueue: print a job during 9 seconds
 thread 4:PrintQueue: print a job during 6 seconds
 thread 5:PrintQueue: print a job during 2 seconds
 thread 6:PrintQueue: print a job during 3 seconds
 thread 7:PrintQueue: print a job during 8 seconds
 thread 8:PrintQueue: print a job during 2 seconds
 thread 9:PrintQueue: print a job during 6 seconds
 thread 0:PrintQueue: print a job during 3 seconds
thread 0 :the document has been printed
 thread 1:PrintQueue: print a job during 0 seconds
thread 1 :the document has been printed
 thread 2:PrintQueue: print a job during 5 seconds
thread 2 :the document has been printed
 thread 3:PrintQueue: print a job during 1 seconds
thread 3 :the document has been printed
 thread 4:PrintQueue: print a job during 1 seconds
thread 4 :the document has been printed
 thread 5:PrintQueue: print a job during 0 seconds
thread 5 :the document has been printed
 thread 6:PrintQueue: print a job during 6 seconds
thread 6 :the document has been printed
 thread 7:PrintQueue: print a job during 0 seconds
thread 7 :the document has been printed
 thread 8:PrintQueue: print a job during 1 seconds
thread 8 :the document has been printed
 thread 9:PrintQueue: print a job during 1 seconds
thread 9 :the document has been printed

It can be seen that the order in which threads directly acquire locks is completely fair, first come, first served.

Operating results under unfair circumstances:

thread 0 :going to print a job
 thread 0:PrintQueue: print a job during 4 seconds
thread 1 :going to print a job
thread 2 :going to print a job
thread 3 :going to print a job
thread 4 :going to print a job
thread 5 :going to print a job
thread 6 :going to print a job
thread 7 :going to print a job
thread 8 :going to print a job
thread 9 :going to print a job
 thread 0:PrintQueue: print a job during 9 seconds
thread 0 :the document has been printed
 thread 1:PrintQueue: print a job during 2 seconds
 thread 1:PrintQueue: print a job during 8 seconds
thread 1 :the document has been printed
 thread 2:PrintQueue: print a job during 3 seconds
 thread 2:PrintQueue: print a job during 7 seconds
thread 2 :the document has been printed
 thread 3:PrintQueue: print a job during 7 seconds
 thread 3:PrintQueue: print a job during 3 seconds
thread 3 :the document has been printed
 thread 4:PrintQueue: print a job during 7 seconds
 thread 4:PrintQueue: print a job during 8 seconds
thread 4 :the document has been printed
 thread 5:PrintQueue: print a job during 2 seconds
 thread 5:PrintQueue: print a job during 9 seconds
thread 5 :the document has been printed
 thread 6:PrintQueue: print a job during 3 seconds
 thread 6:PrintQueue: print a job during 0 seconds
thread 6 :the document has been printed
 thread 7:PrintQueue: print a job during 6 seconds
 thread 7:PrintQueue: print a job during 2 seconds
thread 7 :the document has been printed
 thread 8:PrintQueue: print a job during 7 seconds
 thread 8:PrintQueue: print a job during 4 seconds
thread 8 :the document has been printed
 thread 9:PrintQueue: print a job during 1 seconds
 thread 9:PrintQueue: print a job during 4 seconds
thread 9 :the document has been printed

It can be seen that under unfair circumstances, there is a phenomenon of "jumping in the queue" by robbing the lock. For example, Thread 0 can obtain the lock first after releasing the lock, although Thread 1 ~ Thread 9 are already in the waiting queue.

Fair and unfair comparison

advantageinferiority
Fair lockAll threads are fair and equal. Each thread always has the opportunity to execute after waiting for a period of timeSlower and less throughput
Unfair lockFaster and more throughputThread starvation may occur, that is, some threads cannot be executed for a long time

summary

We have learned about fair and unfair locks. By analyzing the effect of fair and unfair locks through code practice, we can use locks more flexibly in the future.

Topics: Java Back-end