AQS implementation principle

1. AQS basics

         AQS defines a set of synchronization templates for multi-threaded access to shared resources, which solves a large number of details involved in implementing synchronizers.

        AQS consists of three parts, the state synchronization state, the CLH queue composed of Nodes, and the ConditionObject condition variable (including the conditional one-way queue composed of Nodes). The following will introduce these three parts respectively.

First paste the core functions provided by AbstractQueuedSynchronizer, it is enough to be familiar with it, and it will be explained later

state

  • getState(): Returns the synchronization state
  • setState(int newState): Set the synchronization state
  • compareAndSetState(int expect, int update): Use CAS to set the synchronization state
  • isHeldExclusively(): Whether the current thread holds resources

Exclusive resource (does not respond to thread interruption)

  • tryAcquire(int arg): Exclusive acquisition of resources, implemented by subclasses
  • acquire(int arg): Exclusively acquire resource templates
  • tryRelease(int arg): exclusive release of resources, implemented by subclasses
  • release(int arg): exclusive release resource template

Shared resources (do not respond to thread interrupts)

  • tryAcquireShared(int arg): Shared acquisition of resources, if the return value is greater than or equal to 0, the acquisition is successful, otherwise the acquisition fails, and the subclass implements
  • acquireShared(int arg): Shared acquisition resource template
  • tryReleaseShared(int arg): Shared release of resources, implemented by subclasses
  • releaseShared(int arg): Shared release resource template

To add here, the operation of obtaining exclusive and shared resources also provides extended functions for timeout and response interruption. Interested readers can go to the source code of AbstractQueuedSynchronizer to learn more.

Two, CLH queue

        The full name of the CLH queue is (Craig, Landin, and Hagersten) lock queue, which is used to store blocked thread information.

        CLH is a FIFO (first-in, first-out) double-ended bidirectional queue maintained inside AQS (to facilitate insertion of tail nodes), based on a linked list data structure. When a thread fails to compete for resources, the thread waiting for resources will be encapsulated into a Node node, and inserted into the tail of the queue through CAS atomic operations. Finally, different Node nodes are connected to form a CLH queue. Therefore, AQS manages the resource competition through the CLH queue. Thread, personal summary CLH queue has the following advantages:

  • FIFO guarantees fairness;
  • Non-blocking queue, through spin lock and CAS to ensure the atomicity of node insertion and removal, to achieve lock-free fast insertion;
  • The idea of ​​spin lock is adopted, so CLH is also a scalable, high-performance and fair spin lock based on linked list;

Node inner class

        Node is an internal class of AQS. Each thread waiting for resources will be encapsulated into a Node node to form a CLH queue and a waiting queue . Therefore, Node is a very important part, and understanding it is the first step in understanding AQS.

The variables in the Node class are well understood, only waitStatus and nextWaiter are not elaborated, and the following is a supplementary explanation;

The waitStatus waiting status is as follows:

 nextWaiter special mark:

  • When the Node is in the CLH queue, nextWaiter represents a shared or exclusive mark;
  • When Node is in the conditional queue, nextWaiter represents the next Node node pointer;

Process overview

If a thread fails to obtain resources, it is encapsulated into a Node node that enters the queue from the tail of the CLH queue and blocks the thread. When a thread releases resources, it will wake up the thread associated with the Node node at the head of the CLH queue (the head here refers to the second node), and obtain it again resource.

enqueue

Threads that fail to obtain resources need to be encapsulated into Node nodes, and then queued at the end. The addWaiter function is provided in AQS to complete the creation and queue of Node nodes.

private Node addWaiter(Node mode) {
        //根据当前线程创建节点,等待状态为0
        Node node = new Node(Thread.currentThread(), mode);
        // 获取尾节点
        Node pred = tail;
        if (pred != null) {
            //如果尾节点不等于null,把当前节点的前驱节点指向尾节点
            node.prev = pred;
            //通过cas把尾节点指向当前节点
            if (compareAndSetTail(pred, node)) {
                //之前尾节点的下个节点指向当前节点
                pred.next = node;
                return node;
            }
        }
        //如果添加失败或队列不存在,执行enq函数
        enq(node);
        return node;
    }

When adding a node, if the slave CLH queue already exists, quickly add the current node to the end of the queue through CAS; if the addition fails or the queue does not exist, point to the enq function to spin into the queue.

private Node enq(final Node node) {
        for (;;) { //循环
            //获取尾节点
            Node t = tail;
            if (t == null) {
                //如果尾节点为空,创建哨兵节点,通过cas把头节点指向哨兵节点
                if (compareAndSetHead(new Node()))
                    //cas成功,尾节点指向哨兵节点
                    tail = head;
            } else {
                //当前节点的前驱节点设指向之前尾节点
                node.prev = t;
                //cas设置把尾节点指向当前节点
                if (compareAndSetTail(t, node)) {
                    //cas成功,之前尾节点的下个节点指向当前节点
                    t.next = node;
                    return t;
                }
            }
        }
    }

通过自旋CAS尝试往队列尾部插入节点,直到成功;自旋过程如果发现CLH队列不存在时会初始化CLH队列,入队过程流程如下图(队列不存在流程图):

 第一次循环

  1. 刚开始C L H队列不存在,head与tail都指向null
  2. 要初始化C L H队列,会创建一个哨兵节点,head与tail都指向哨兵节点

第二次循环

  1. 当前线程节点的前驱节点指向尾部节点(哨兵节点) 
  2. 设置当前线程节点为尾部,tail指向当前线程节点 
  3. 前尾部节点的后驱节点指向当前线程节点(当前尾部节点)

最后结合addWaiter与enq函数的入队流程图如下:

出队

CLH队列中的节点都是获取资源失败的线程节点,当持有资源的线程释放资源时,会将head.next指向的线程节点唤醒(C L H队列的第二个节点),如果唤醒的线程节点获取资源成功,线程节点清空信息设置为头部节点(新哨兵节点),原头部节点出队(原哨兵节点)

acquireQueued函数中的部分代码

final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            // 0.自旋阻塞等待获取资源
            for (;;) {
                //1.获取前驱节点
                final Node p = node.predecessor();
                //如果前驱节点是首节点,获取资源(子类实现)
                if (p == head && tryAcquire(arg)) {
                    //2.获取资源成功,设置当前节点为头节点,清空当前节点的信息,把当前节点变成哨兵节点
                    setHead(node);
                    //3.原来首节点下个节点指向为null
                    p.next = null; // help GC
                    //4.非异常状态,防止指向finally逻辑
                    failed = false;
                    //5.返回线程中断状态
                    return interrupted;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }


private void setHead(Node node) {
    //节点设置为头部
    head = node;
    //清空线程
    node.thread = null;
    //清空前驱节点
    node.prev = null;
}

只需要关注1~3步骤即可,过程非常简单,假设获取资源成功,更换头部节点,并把头部节点的信息清除变成哨兵节点,注意这个过程是不需要使用CAS来保证,因为只有一个线程能够成功获取到资源。

三、条件变量

Object的wait、notify函数是配合Synchronized锁实现线程间同步协作的功能,AQS的ConditionObject条件变量也提供这样的功能,通过ConditionObject的await和signal两类函数完成。不同于Synchronized锁,一个AQS可以对应多个条件变量,而Synchronized只有一个。

As shown in the figure below, ConditionObject maintains a one-way condition queue internally. Unlike the CHL queue, the condition queue only enqueues the thread nodes that execute await, and the nodes that join the condition queue cannot be in the CHL queuethe nodes that are dequeued from the condition queue , will be enqueued to the CHL queue.

When a thread executes the await function of ConditionObject and blocks the current thread, the thread will be encapsulated into a Node node and added to the end of the condition queue, and other threads execute the signal function of ConditionObject, and the thread node at the head of the condition queue will be transferred to the CHL queue to participate Competitive resources, the specific process is as follows:

Finally, add that the conditional queue Node class uses the nextWaiter variable to point to the next node, and because it is a one-way queue, the prev and next variables are both null.

4. Advanced

AQSThe template method design pattern is adopted, and two types of templates are provided, one is an exclusive template, and the other is a shared shape pattern, and the corresponding template functions are as follows:

exclusive formula

  • acquire acquires resources
  • releaseRelease resources

Shared

  • acquireShared to acquire resources
  • releaseShared releases resources

exclusive access to resources 

acquire is a template function. The template process is that the thread acquires shared resources. If the resource is successfully acquired, the thread returns directly, otherwise it enters the CLH queue until the resource is successfully acquired, and the entire process ignores the impact of interruption. The acquire function code is as follows:

  1.  Execute the tryAcquire function. tryAcquire is implemented by a subclass, which represents whether the resource acquisition is successful. If the resource acquisition fails, execute the following logic;
  2. Execute the addWaiter function (introduced earlier), create an exclusive node according to the current thread, and put it into the CLH queue;
  3. Execute the acquireQueued function, spin blocking and wait for resource acquisition;
  4. If the resource is successfully acquired in the acquireQueued function, the execution of the thread interruption logic is determined according to whether the thread is interrupted or not;

The general flow of the acquire function is clear. Let’s analyze the acquireQueued function. After the thread is encapsulated into a node, how does it spin and block waiting to acquire resources? The code is as follows:

final boolean acquireQueued(final Node node, int arg) {
        //异常状态,默认是
        boolean failed = true;
        try {
            //该线程是否中断过,默认否
            boolean interrupted = false;
            for (;;) {//自旋
                //获取前驱节点
                final Node p = node.predecessor();
                //如果前驱节点是首节点,获取资源(子类实现)
                if (p == head && tryAcquire(arg)) {
                    //获取资源成功,设置当前节点为头节点,清空当前节点的信息,把当前节点变成哨兵节点
                    setHead(node);
                    //原来首节点下个节点指向为null
                    p.next = null; // help GC
                    //非异常状态,防止指向finally逻辑
                    failed = false;
                    //返回线程中断状态
                    return interrupted;
                }
                /**
                 * 如果前驱节点不是首节点,先执行shouldParkAfterFailedAcquire函数,shouldParkAfterFailedAcquire做了三件事
                 * 1.如果前驱节点的等待状态是SIGNAL,返回true,执行parkAndCheckInterrupt函数,返回false
                 * 2.如果前驱节点的等待状态是CANCELLED,把CANCELLED节点全部移出队列(条件节点)
                 * 3.以上两者都不符合,更新前驱节点的等待状态为SIGNAL,返回false
                 */
                if (shouldParkAfterFailedAcquire(p, node) &&
                    //使用LockSupport类的静态方法park挂起当前线程,直到被唤醒,唤醒后检查当前线程是否被中断,返回该线程中断状态并重置中断状态
                    parkAndCheckInterrupt())
                    //该线程被中断过
                    interrupted = true;
                }
            } finally {
                // 尝试获取资源失败并执行异常,取消请求,将当前节点从队列中移除
                if (failed)
                    cancelAcquire(node);
            }
    }

The core flow chart is as follows:

exclusive release of resources

To obtain resources, it is necessary to release resources. AQS provides a release template function to release resources. The template process is that the thread releases resources successfully and wakes up the second thread node of the CLH queue (the next node of the first node). The code is as follows:

 public final boolean release(int arg) {

        if (tryRelease(arg)) {//释放资源成功,tryRelease子类实现
            //获取头部线程节点
            Node h = head;
            if (h != null && h.waitStatus != 0) //头部线程节点不为null,并且等待状态不为0
                //唤醒CHL队列第二个线程节点
                unparkSuccessor(h);
            return true;
        }
        return false;
    }
    
    
    private void unparkSuccessor(Node node) {
        //获取节点等待状态
        int ws = node.waitStatus;
        if (ws < 0)
            //cas更新节点状态为0
            compareAndSetWaitStatus(node, ws, 0);
    
        //获取下个线程节点        
        Node s = node.next;
        if (s == null || s.waitStatus > 0) { //如果下个节点信息异常,从尾节点循环向前获取到正常的节点为止,正常情况不会执行
            s = null;
            for (Node t = tail; t != null && t != node; t = t.prev)
                if (t.waitStatus <= 0)
                    s = t;
        }
        if (s != null)
            //唤醒线程节点
            LockSupport.unpark(s.thread);
        }
    }

The release logic is very simple, the flow chart is as follows:

Shared access to resources

acquireShared is a template function. The template process is that the thread acquires shared resources. If the resource is acquired, the thread returns directly, otherwise it enters the CLH queue until the resource is acquired, and the entire process ignores the impact of interruption. The acquireShared function code is as follows:

  public final void acquireShared(int arg) {
        /**
         * 1.负数表示失败
         * 2.0表示成功,但没有剩余可用资源
         * 3.正数表示成功且有剩余资源
         */
        if (tryAcquireShared(arg) < 0) //获取资源失败,tryAcquireShared子类实现
            //自旋阻塞等待获取资源
            doAcquireShared(arg);
    }

The logic of the doAcquireShared function is basically the same as that of the exclusive acquireQueued function. The only difference is the red box in the figure below:

  1. Nodes are tagged with shared
  2. If the resources are successfully obtained, subsequent resources will be awakened, because the number of resources may be > 0, which means that there are still resources available, so subsequent thread nodes need to be awakened

Shared release of resources

AQS provides the releaseShared template function to release resources. The template process is that the thread releases resources successfully and wakes up the second thread node (next to the first node) of the CHL queue. The code is as follows:

public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) {//释放资源成功,tryReleaseShared子类实现
            //唤醒后继节点
            doReleaseShared();
            return true;
        }
        return false;
    }
    
    private void doReleaseShared() {
        for (;;) {
            //获取头节点
            Node h = head;
            if (h != null && h != tail) {
                int ws = h.waitStatus;
    
                if (ws == Node.SIGNAL) {//如果头节点等待状态为SIGNAL
                    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))//更新头节点等待状态为0
                        continue;            // loop to recheck cases
                    //唤醒头节点下个线程节点
                    unparkSuccessor(h);
                }
                //如果后继节点暂时不需要被唤醒,更新头节点等待状态为PROPAGATE
                else if (ws == 0 &&
                         !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                    continue;               
            }
            if (h == head)              
                break;
        }
    }

It is not much different from the exclusive release of resources. They both wake up the next node of the head node, so I won’t describe it too much.

Five, actual combat

AQS defines a set of synchronization templates for multi-threaded access to shared resources, which solves a large number of detailed problems involved in the implementation of synchronizers, and can greatly reduce the implementation work. Now we implement a non-reentrant exclusive lock based on AQS, and directly use AQS to provide Exclusive templates, only need to clarify the semantics of state and implement the tryAcquire and tryRelease functions (acquire resources and release resources), where the state is 0 means that the lock is not held by the thread, and the state is 1 means that the lock is already held by a thread , since it is a non-reentrant lock, there is no need to record the number of times the lock-holding thread acquires the lock.

The non-reentrant exclusive lock code is as follows:

public class NonReentrantLock implements Lock {


    /**
     * @Author 程序猿阿星
     * @Description 自定义同步器
     */
    private static class Sync extends AbstractQueuedSynchronizer {

        /**
         * 锁是否被线程持有
         */
        @Override
        protected boolean isHeldExclusively() {
            //0:未持有 1:已持有
            return super.getState() == 1;
        }

        /**
         * 获取锁
         */
        @Override
        protected boolean tryAcquire(int arg) {
            if (arg != 1) {
                //获取锁操作,是需要把state更新为1,所以arg必须是1
                throw new RuntimeException("arg not is 1");
            }
            if (compareAndSetState(0, arg)) {//cas 更新state为1成功,代表获取锁成功
                //设置持有锁线程
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            }
            return false;
        }

        /**
         * 释放锁
         */
        @Override
        protected boolean tryRelease(int arg) {
            if (arg != 0) {
                //释放锁操作,是需要把state更新为0,所以arg必须是0
                throw new RuntimeException("arg not is 0");
            }
            //清空持有锁线程
            setExclusiveOwnerThread(null);
            //设置state状态为0,此处不用cas,因为只有获取锁成功的线程才会执行该函数,不需要考虑线程安全问题
            setState(arg);
            return true;
        }

        /**
         * 提供创建条件变量入口
         */
        public ConditionObject createConditionObject() {
            return new ConditionObject();
        }

    }

    private final Sync sync = new Sync();

    /**
     * 获取锁
     */
    @Override
    public void lock() {
        //Aqs独占式-获取资源模板函数
        sync.acquire(1);
    }
        
    /**
     * 获取锁-响应中断
     */
    @Override
    public void lockInterruptibly() throws InterruptedException {
        //Aqs独占式-获取资源模板函数(响应线程中断)
        sync.acquireInterruptibly(1);
    }

    /**
     * 获取锁是否成功-不阻塞
     */
    @Override
    public boolean tryLock() {
        //子类实现
        return sync.tryAcquire(1);
    }
    
    /**
     * 获取锁-超时机制
     */
    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        //Aqs独占式-获取资源模板函数(超时机制)
        return sync.tryAcquireNanos(1,unit.toNanos(time));
    }
    
    /**
     * 释放锁
     */
    @Override
    public void unlock() {
        //Aqs独占式-释放资源模板函数
        sync.release(0);
    }
    
    /**
     * 创建条件变量
     */
    @Override
    public Condition newCondition() {
        return sync.createConditionObject();
    }
}

NonReentrantLock defines an internal class Sync. Sync is used to implement specific lock operations. It inherits AQS. Because it uses an exclusive template, it rewrites the tryAcquire and tryRelease functions. In addition, it provides an entry to create a condition variable, which is used below A custom exclusive lock to synchronize two threads on j++.

private static int j = 0;

    public static void main(String[] agrs) throws InterruptedException {
        NonReentrantLock  nonReentrantLock = new NonReentrantLock();

        Runnable runnable = () -> {
            //获取锁
            nonReentrantLock.lock();
            for (int i = 0; i < 100000; i++) {
                j++;
            }
            //释放锁
            nonReentrantLock.unlock();
        };

        Thread thread = new Thread(runnable);
        Thread threadTwo = new Thread(runnable);

        thread.start();
        threadTwo.start();

        thread.join();
        threadTwo.join();

        System.out.println(j);
    }
    

6. AQS Simplified Flowchart

おすすめ

転載: blog.csdn.net/summer_fish/article/details/120080319#comments_27859909