The essence of Java Concurrent "lock" (realizing lock step by step)

Posted by maneetpuri on Fri, 17 Dec 2021 14:36:47 +0100

preface

After analyzing CAS and thread hang / wake-up related knowledge in the previous article, we need to analyze the source code of Synchronized and AQS in this article. However, this time I do not intend to do so. Doing so will fall into the boring explanation of the source code, do not understand the causes and consequences, and the turning point is too rigid. Therefore, this paper first deduces why "lock" is needed step by step from the actual demand? How to realize the "lock" and other steps by yourself, and finally naturally transition to the "lock" provided by the system and its principle and application.
Through this article, you will learn:

1. Mutually exclusive access variable
2. CAS access variable
3. Mutually exclusive access critical area
4. Thread suspend / wake policy
5. Thread synchronization strategy
6. Summary

1. Mutually exclusive access variable

Let's start with a code:

    static int a = 0;
    private static void inc() {
        a++;
    }

As mentioned above, thread 1 and thread 2 access this code at the same time and perform self increment operation on the shared variable A. We know that the result of a is uncontrollable.

In order to control the result, the thread must not operate on a at the same time, that is, it needs mutually exclusive access to a.

2. CAS access variable

According to the analysis in the previous article, CAS can access shared variables mutually exclusive, so the code is changed as follows:

    static AtomicInteger a = new AtomicInteger(0);
    private static void inc() {
        a.incrementAndGet();
    }

AtomicInteger packages variable a, and its bottom layer accesses variable a through CAS mutual exclusion, so multithreading mutual exclusion is realized.
CAS details, please move to: Application and principle of Java Unsafe/CAS/LockSupport

3. Mutually exclusive access critical area

The above analysis focuses on the scenario of multiple threads accessing a single variable. Consider another case: when the variables requiring mutually exclusive access are not only a, but also b, c and other variables, you may do this:

    static AtomicInteger a = new AtomicInteger(0);
    static AtomicInteger b = new AtomicInteger(0);
    static AtomicInteger c = new AtomicInteger(0);
    private static void inc() {
        a.incrementAndGet();
        b.incrementAndGet();
        c.incrementAndGet();
    }

The number of shared variables requires as many AtomicInteger wrappers. Now there are only three shared variables. What if there are more? This obviously does not meet further needs.
Since a, b and c all need mutually exclusive access, can we do mutually exclusive processing at the entrance?

Enter critical zone

The operation of multiple shared variables is put into the critical area, so the problem comes. How to realize the mutually exclusive access of the critical area? We still think of CAS.

Set the shared variable x with an initial value of 0. Each thread that wants to enter the critical area must first access X and modify x to 1. If it is successful, it can enter the critical area. Otherwise, it will keep trying to modify in an endless loop.

The code is as follows:

    static AtomicInteger x = new AtomicInteger(0);
    static int a = 0;
    static int b = 0;
    static int c = 0;
    private static void inc() {
        //Try to change x from 0 to 1. If it fails, try again all the time
        while(!x.compareAndSet(0, 1));
        //If you go here, it means that the modification has been successful
        {
            //Critical zone
            a++;
            b++;
            c++;   
        }
    }

When multiple threads enter the inc() method at the same time, first try to modify the value of X. if it is successful, exit the loop, otherwise try consistently. When one thread successfully changes x from 0 to 1, other threads will fail to continue trying this operation. Only the thread that successfully modifies the x value can enter the critical area, so the mutually exclusive access to the critical area has been realized.

Exit critical zone

Threads entering the critical zone always exit. When exiting, X needs to be modified back so that other threads can enter the critical zone. Therefore, the operation of releasing x is added:

    static AtomicInteger x = new AtomicInteger(0);

    static int a = 0;
    static int b = 0;
    static int c = 0;
    private static void inc() {
        //Try to change x from 0 to 1. If it fails, try again all the time
        while(!x.compareAndSet(0, 1));
        //If you go here, it means that the modification has been successful
        {
            //Critical zone
            a++;
            b++;
            c++;
        }
        //There is no need to keep trying here, because there is always only one thread accessing at the same time
        x.compareAndSet(1, 0);
    }

It can be seen from the above that before entering the critical area, modify the value of X with CAS, and enter the critical area after successful modification. After exiting the critical area, CAS modifies x to return to the original value, which realizes the process of mutually exclusive access to the critical area.
If you want to access any critical area, you can use this method. Think about whether it is equivalent to getting the "lock" before entering the critical area. Other threads that do not get the "lock" always try to get the "lock". When the thread with the lock exits the critical area, release the "lock", and it can get the "lock".

Abstract the code of "lock" and access the critical area as follows:

    static int a = 0;
    static int b = 0;
    static int c = 0;
    //Construct Lock object
    static Lock lock = new MyLock();
    private static void inc() {
        //Acquire lock
        lock.lock();
        {
            //Critical zone
            a++;
            b++;
            c++;
        }
        //Release lock
        lock.unlock();
    }
    
    //Abstract lock structure
    interface Lock {
        void lock();
        void unlock();
    }
    
    static class MyLock implements Lock{
        AtomicInteger x = new AtomicInteger(0);
        
        @Override
        public void lock() {
            while(!x.compareAndSet(0, 1));
        }
        
        @Override
        public void unlock() {
            x.compareAndSet(1, 0);
        }
        
    }

Therefore, the steps of mutually exclusive access to the critical area are as follows:

1. Acquire lock
2. Enter critical zone
3. Exit critical zone
4. Release lock

4. Thread suspend / wake policy

Taking two threads accessing the critical area as an example, when thread 1 successfully obtains the lock and enters the critical area, thread 2 cannot get the lock, but will try all the time. Imagine:

  • Not only do two threads compete for locks, but many threads compete for locks at the same time.
  • The critical area takes a long time to execute, and it is difficult to release the lock.

So the thread that did not get the lock has been trying to get it in an infinite loop, which is a waste of CPU. Can you let the thread that did not get the lock hang first and wake it up when the lock is released?

Thread pending

Threads that fail to compete for locks suspend themselves first. How can other threads that release locks find previously suspended threads? Put the suspended thread into the queue. When another thread releases the lock, take out the suspended thread from the queue and wake it up. The awakened thread continues to compete for the lock.

The process is clear. Let's see how to implement it in Code:

    static class MyLock implements Lock{
        AtomicInteger x = new AtomicInteger(0);
        
        //Blocking queue
        LinkedBlockingQueue<Thread> blockList = new LinkedBlockingQueue<>();

        @Override
        public void lock() {
            //The while loop is used to continue to compete for locks after the thread is awakened
            while (true) {
                if (x.get() > 0) {
                    //Indicates that a thread is already holding a lock
                    //Reentry is not considered here for the time being
                } else {
                    //Unlocked state
                    if (x.compareAndSet(0, 1)) {
                        //Lock acquired successfully
                        return;
                    } else {
                        //Failed to acquire lock
                    }
                }
                //This means that the lock was not obtained
                //Join queue
                blockList.offer(Thread.currentThread());
                //Suspend thread
                LockSupport.park();   
            }
        }
    }

The above is the process of locking:

1. First judge whether the current lock is available. If it is available, obtain the lock.
2. If successful, exit; otherwise, join the blocking queue and suspend yourself.

Thread wakeup

Let's see how to wake up a suspended thread:

        @Override
        public void unlock() {
            if (x.get() > 0) {
                //Description lock currently held
                //Release lock
                x.compareAndSet(1, 0);
                //Remove thread wakeup from blocking queue
                //Take the team head element here
                Thread thread = blockList.poll();
                if (thread != null) {
                    LockSupport.unpark(thread);
                }
            }
        }

The above is the unlocking process:

1. First judge whether the lock is currently held. If so, release the lock.
2. After the lock is released, the thread is taken out of the blocking queue to wake up.

After the thread holding the lock wakes up the blocked thread from the blocking queue, the awakened thread continues to try to compete for the lock.

For details of LockSupport, please move to: Application and principle of Java Unsafe/CAS/LockSupport

5. Thread synchronization strategy

The above analysis is that multiple threads are mutually exclusive to access the critical area. The operations of these threads on the critical area are only mutually exclusive and have no other dependencies. Imagine a scenario:

1. Thread 1 performs auto increment on variable a, and pauses auto increment when a increases to 10.
2. Thread 2 performs self decrement on variable b. when a decreases to 0, the self decrement is suspended.

combination Java thread Foundation , we know that thread 1 and thread 2 have a synchronization relationship, and we also know that thread synchronization requires locking and mutually exclusive access to condition variables. Therefore, there are the following relationships:

As can be seen from the figure:

1. When you need to wait, go to the red line. Put the thread into the waiting queue, release the lock, and wake up the thread in the blocking queue.
2. When notification is needed, go to the green line to remove the thread from the waiting queue and add it to the blocking queue.
3. The process of thread synchronization adds a waiting queue to the process of thread mutual exclusion, which stores the threads suspended for waiting due to certain conditions. When another thread finds that the conditions are met, it notifies the thread in the waiting queue to continue working.

Add synchronization related codes on the basis of thread mutually exclusive codes:
first
Abstract out the wait notification interface first:

    //Abstract synchronization waiting and notification mechanism
    interface Condition {
        void waitCondition();

        void notifyCondition();
    }

secondly
Implement this interface:

    static class MyLock implements Lock {
        AtomicInteger x = new AtomicInteger(0);

        //Blocking queue
        LinkedBlockingQueue<Thread> blockList = new LinkedBlockingQueue<>();

        public Condition newCondition() {
            return new MyCondition();
        }

        @Override
        public void lock() {
            //The while loop is used to continue to compete for locks after the thread is awakened
            while (true) {
                if (x.get() > 0) {
                    //Indicates that a thread is already holding a lock
                    //Reentry is not considered here for the time being
                } else {
                    //Unlocked state
                    if (x.compareAndSet(0, 1)) {
                        //Lock acquired successfully
                        return;
                    } else {
                        //Failed to acquire lock
                    }
                }
                //This means that the lock was not obtained
                //Join queue
                blockList.offer(Thread.currentThread());
                //Suspend thread
                LockSupport.park();
            }
        }

        @Override
        public void unlock() {
            if (x.get() > 0) {
                //Description lock currently held
                //Release lock
                x.compareAndSet(1, 0);
                //Remove thread wakeup from blocking queue
                //Take the team head element here
                Thread thread = blockList.poll();
                if (thread != null) {
                    LockSupport.unpark(thread);
                }
            }
        }

        class MyCondition implements Condition {
            LinkedBlockingQueue<Thread> waitList = new LinkedBlockingQueue<>();

            //Wait - Notification
            @Override
            public void waitCondition() {
                //Join the waiting queue
                waitList.add(Thread.currentThread());

                //Release the lock to give other threads a chance to acquire the lock
                x.compareAndSet(1, 0);
                Thread thread = blockList.poll();
                if (thread != null) {
                    LockSupport.unpark(thread);
                    //Suspend current thread
                    LockSupport.park();
                }
            }

            @Override
            public void notifyCondition() {
                //After receiving the notice, it indicates that the waiting conditions are met
                //Remove thread from waiting queue
                Thread thread = waitList.poll();
                //And add the thread to the blocking queue
                if (thread != null)
                    blockList.offer(thread);
            }
        }
    }

And provide a method to get the interface: newCondition().

last
Let's see how to use it:

    public static void main(String args[]) {
        try {
            Thread t1 = new Thread(new Runnable() {
                @Override
                public void run() {
                    inc();
                }
            }, "t1");

            Thread t2 = new Thread(new Runnable() {
                @Override
                public void run() {
                    sub();
                }
            }, "t2");

            t1.start();
            t2.start();

        } catch (Exception e) {

        }
    }
    static int a = 0;
    //Construct Lock object
    static MyLock lock = new MyLock();
    static Condition conditionInc = lock.newCondition();
    static Condition conditionSub = lock.newCondition();


    private static void inc() {
        try {
            while (true) {
                //Acquire lock
                lock.lock();
                {
                    System.out.println("lock suc in " + Thread.currentThread().getName());
                    if (a < 10) {
                        a++;
                        conditionSub.notifyCondition();
                        System.out.println("notify: a:" + a + " in " + Thread.currentThread().getName());
                    } else {
                        //Block wait and release the lock
                        System.out.println("wait: a:" + a + " in " + Thread.currentThread().getName());
                        conditionInc.waitCondition();
                    }
                }
                //Release lock
                lock.unlock();
            }
        } catch (Exception e) {

        }
    }

    private static void sub() {
        try {
            while (true) {
                lock.lock();
                System.out.println("lock suc in " + Thread.currentThread().getName());
                if (a == 0) {
                    System.out.println("wait: a:" + a + " in " + Thread.currentThread().getName());
                    conditionSub.waitCondition();
                } else {
                    a--;
                    conditionInc.notifyCondition();
                    System.out.println("notify: a:" + a + " in " + Thread.currentThread().getName());
                }
                lock.unlock();
            }
        } catch (Exception e) {

        }
    }

The Demo implements the following functions:

1. Thread 1 increases the shared variable a automatically. When a < 10, it always increases automatically, and informs thread 2 to decrease a automatically. When a > = 10, it will not increase automatically, and wait in place for the value of a to decrease.
2. Thread 2 decreases the shared variable automatically. When a > 0, it always decreases automatically, and informs thread 1 to increase a automatically.
When a=0, there is no self subtraction and wait in place for the value of a to increase.
3. Orderly cooperation between two threads is realized through waitCondition/notifyCondition. In fact, it is a typical producer consumer model.

Matters needing attention:

When waitCondition is called, the lock is released and the thread on the blocking queue is awakened.
When notifyCondition is called, the lock is not released and the thread on the blocking queue is not awakened.

6. Summary

The above describes the origin of "lock" step by step and how to realize "lock" from thread mutually exclusive access to shared variables, CAS access to shared variables, thread mutually exclusive access to critical area, thread mutually exclusive lock, and finally thread synchronization.
Of course, this "lock" is only the most basic and simplest "lock", without considering reentry, interrupt cancellation, fair and unfair preemption, performance improvement of some competitive locks, etc. But it includes the basic idea of "lock":

Thread suspend / wake + CAS + block / wait queue

After understanding the basic idea, you can easily understand the lock implementation provided by the system – > AQS and Synchronized.
The next chapter officially enters the analysis of AQS and Synchronized.

If you like it, please praise and pay attention. Your encouragement is my driving force

In the process of continuous updating, work with me step by step to learn more about Android

Topics: Java