[Concurrent programming] AbstractQueuedSynchronizer (AQS) synchronizer

1 Introduction

Most synchronization classes in Java (Lock, Semaphore, ReentrantLock, etc.) are implemented based on AbstractQueuedSynchronizer (referred to as AQS). AQS is a simple framework that provides atomically managing synchronization status, blocking and waking up threads, and a queue model. This article will gradually deepen from the application layer to the principle layer, and through the basic characteristics of ReentrantLock and the relationship between ReentrantLock and AQS, to deeply interpret the knowledge points of AQS-related exclusive locks.

2. ReentrantLock

ReentrantLock supports fair locks and unfair locks, and the bottom layer of ReentrantLock is implemented by AQS. So how does ReentrantLock relate to AQS through fair locks and unfair locks?

2.1 code-1

The implementation class of the Lock interface basically completes thread access control through [aggregation] a subclass of [queue synchronizer]

    private final Lock lock = new ReentrantLock();

    // 从reentrant 分析 AQS
    public void m() {
        lock.lock();  // block until condition holds
        try {
            // ... method body
        } finally {
            lock.unlock();
        }
    }

2.2 Overall structure

2.3 Look at fair locks and unfair locks through the source code of ReentrantLock  

  • Fair lock and unfair lock creation

å¨è¿éæå ¥ å¾çæè¿ °

  • Realize difference comparison

å¨è¿éæå ¥ å¾çæè¿ °

It can be clearly seen that the only difference between the lock() method of fair lock and unfair lock is that the fair lock has an additional restriction when acquiring the synchronization state: hasQueuedPredecessors()

  • HasQueuedPredecessors is a method for judging whether there are valid nodes in the waiting queue when the fair lock is locked. If it returns False, it means that the current thread can fight for shared resources; if it returns True, it means that there are valid nodes in the queue, and the current thread must be added to the waiting queue (predecessor node)
    public final boolean hasQueuedPredecessors() {
        // The correctness of this depends on head being initialized
        // before tail and on head.next being accurate if the current
        // thread is first in queue.
        Node t = tail; // Read fields in reverse initialization order
        Node h = head;
        Node s;
        return h != t &&((s = h.next) == null || s.thread != Thread.currentThread());
    }

Seeing this, let us understand h != t && ((s = h.next) == null || s.thread != Thread.currentThread()); Why should we judge the next node of the head node? What is the data stored in the first node?

In the doubly linked list, the first node is a virtual node (sentinel node), which does not actually store any information, but only a place. The real first node with data starts at the second node.

  1. When h != t:
  • If (s = h.next) == null, the waiting queue is being initialized by a thread, but only when the Tail points to the Head, and the Head is not pointed to the Tail. At this time, there are elements in the queue and it needs to return True (see below for details) Code analysis).
  • If (s = h.next) != null, it means that there is at least one valid node in the queue at this time.

The second step of judgment:

  • If s.thread == Thread.currentThread() at this time, indicating that the thread in the first valid node of the waiting queue is the same as the current thread, then the current thread can obtain resources;
  • If s.thread != Thread.currentThread(), it means that the first valid node thread of the waiting queue is different from the current thread, and the current thread must be added to the waiting queue

Don't understand, let's see what exactly is done inside AQS...

3.  AbstractQueuedSynchronizer

AQS  uses a variant of the built-in CLH (FIFO) queue to complete the queuing work of resource acquisition threads, and encapsulates each thread that is about to seize resources into a Node node to realize lock allocation.

There is an int class variable (status) that represents the status of the lock, and the modification of the status value is completed through CAS (0 means no, 1 means blocked)

  • Application areas: ReentrantLock | CountDownLatch | ReentrantReadWriteLock | Semaphore

3.1 Synchronizer structure

å¨è¿éæå ¥ å¾çæè¿ °

  • Lock: For the user of the lock (defines the use layer API for the interaction between the programmer and the lock, hides the implementation details, you can call it)

  • Synchronizer: For implementers of locks (such as the Java concurrency god Douglee, who proposes a unified specification and simplifies the implementation of locks, shielding synchronization state management, blocking thread queuing and notification, wake-up mechanisms, etc.)

  • If shared resources are occupied, a certain blocking wait for wake-up mechanism is required to ensure lock allocation. This mechanism is mainly implemented by a variant of the CLH queue, adding threads that cannot temporarily obtain locks into the queue. This queue is an abstract expression of AQS. It encapsulates the thread requesting shared resources into the node (Node) of the queue, and maintains the state of the state variable through CAS, spin and LockSuport.park(), so that the concurrency can achieve the effect of synchronization.

3.2 CLH queue (composed of the names of three big cows), a two-way queue

å¨è¿éæå ¥ å¾çæè¿ °

After the lock is released, the original sentinel node will be set to null, help GC to recycle, and the head node will be reset to the sentinel node

3.3 Internal code structure

å¨è¿éæå ¥ å¾çæè¿ °

(1) Node node

4. Use lock/unlock as a case breakthrough analysis

Pre-knowledge

  • There is a variable called State in AQS. How many values ​​does it have? 3 states: 0 is not occupied, 1 is occupied, and greater than 1 is reentrant lock
  • If the two threads AB come in, how many Node nodes are there in total? The answer is 3, of which the first in the queue is a puppet node (sentinel node)

lock() method call link

  1. lock()
  2. acquire()
  3. tryAcquire (arg)
  4. addWaiter(Node.EXCLUSIVE)
  5. acquireQueued(addWaiter(Node.EXCLUSIVE), arg)

Case scenario

public class AQSDemo {
    public static void main(String[] args) {
        ReentrantLock lock = new ReentrantLock();
        //带入一个银行办理业务的案例来模拟我们的AQS如何进行线程的管理和通知唤醒机制
        //3个线程模拟3个来银行网点,受理窗口办理业务的顾客
        //A顾客就是第一个顾客,此时受理窗口没有任何人,A可以直接去办理
        new Thread(() -> {
                lock.lock();
                try{
                    System.out.println("-----A thread come in");

                    try { TimeUnit.MINUTES.sleep(20); }catch (Exception e) {e.printStackTrace();}
                }finally {
                    lock.unlock();
                }
        },"A").start();

        //第二个顾客,第二个线程---》由于受理业务的窗口只有一个(只能一个线程持有锁),此时B只能等待,
        //进入候客区
        new Thread(() -> {
            lock.lock();
            try{
                System.out.println("-----B thread come in");
            }finally {
                lock.unlock();
            }
        },"B").start();

        //第三个顾客,第三个线程---》由于受理业务的窗口只有一个(只能一个线程持有锁),此时C只能等待,
        //进入候客区
        new Thread(() -> {
            lock.lock();
            try{
                System.out.println("-----C thread come in");
            }finally {
                lock.unlock();
            }
        },"C").start();
    }
}

4.1 lock() method

   /**
     * Sync object for non-fair locks
     */
    static final class NonfairSync extends Sync {
        private static final long serialVersionUID = 7316153563782823691L;

        /**
         * Performs lock.  Try immediate barge, backing up to normal
         * acquire on failure.
         */
        final void lock() {
            // 第一个线程抢占
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                // 第二个线程及后续线程抢占
                acquire(1);
        }

        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
    }

4.2 acquire( ): Source code and three major process trends

å¨è¿éæå ¥ å¾çæè¿ °

(1) tryAcquire (arg)

  • Going in the direction of unfair lock this time

   å¨è¿éæå ¥ å¾çæè¿ °

  • nonfairTryAcquire(acquires) : return false (continue to advance the condition, go to the next step method addWaiter), return true (end)

    å¨è¿éæå ¥ å¾çæè¿ °

(2)addWaiter(Node.EXCLUSIVE)

Suppose ThreadC thread number 3 comes in
(1). prev
(2).compareAndSetTail
(3).next

  • addWaiter(Node mode )

In the doubly linked list, the first node is a virtual node (also called a sentinel node), which does not actually store any information, but just a place. The real first node with data starts from the second node

å¨è¿éæå ¥ å¾çæè¿ °

  • enq(node);

å¨è¿éæå ¥ å¾çæè¿ °

  • The B and C threads are all lined up. The renderings are as follows:

å¨è¿éæå ¥ å¾çæè¿ °

(3)acquireQueued(addWaiter(Node.EXCLUSIVE), arg)

  • acquireQueued : The following methods will be called: shouldParkAterFailedAcquire and parkAndCheckInterrupt | setHead(node))
 final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true; // 标记是否成功获取锁
        try {
            boolean interrupted = false; // 标记线程是否被中断过
            for (;;) {
                final Node p = node.predecessor(); // 获取前驱节点
                //如果前驱是head,即该结点已成老二,那么便有资格去尝试获取锁
                if (p == head && tryAcquire(arg)) {
                    setHead(node); // // 获取成功,将当前节点设置为head节点
                    p.next = null; // help GC  // 原head节点出队,在某个时间点被GC回收
                    failed = false; // //获取成功
                    return interrupted; // 返回是否被中断过
                }
                // 判断获取失败后是否可以挂起,若可以则挂起
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    // 线程若被中断,设置interrupted为true
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }
  • shouldParkAfterFailedAcquire

    å¨è¿éæå ¥ å¾çæè¿ °

  • parkAndCheckInterrupt

    å¨è¿éæå ¥ å¾çæè¿ °

  • When we execute ③ in the figure below, it means that thread B or C has obtained permit.

   å¨è¿éæå ¥ å¾çæè¿ °

  • setHead() method

   å¨è¿éæå ¥ å¾çæè¿ °

4.2 unlock() release the lock

(1)release | tryRelease | unparkSuccessor(h);

    å¨è¿éæå ¥ å¾çæè¿ °

  • tryRelease()

 å¨è¿éæå ¥ å¾çæè¿ °

  • unparkSuccessor( )

  å¨è¿éæå ¥ å¾çæè¿ °

At this point, you can get permission after executing unlock(), and spin into the operation of the next node:
å¨è¿éæå ¥ å¾çæè¿ °

related articles

  1. AQS flow chart analysis: https://www.processon.com/view/5fb6590f7d9c0857dda50442
  2. Reentrant lock (recursive lock) + LockSupport + AQS source code analysis

 

 

 

 

Guess you like

Origin blog.csdn.net/qq_41893274/article/details/113807879