AQS (抽象同步队列)

一、 AQS简介

AQS(AbstractQueuedSynchronizer)抽象同步队列的简称,并发包中锁的底层实现大多是使用AQS实现的。大多数开发者可能不会直接使用AQS,但是知道其原理对架构设计还是很有帮助的。
在这里插入图片描述
图中 f 代表fields, p代表properties, fields指的是私有的不可被改变的, properties指的是可以被外部其他访问者改变的(或者通过set、get方法,或者直接可以改变)。

AQS

下图为AQS的继承层次
AQS的继承层次

AQS是个FIFO的双向队列,通过head和tail来记录队首和队尾元素,队列元素类型为Node。其中Node中的thread变量用来存放进入AQS队列里面的线程。SHARED变量用来标记该线程是获取共享资源时被阻塞挂起后放入AQS队列中的,EXCLUSIVE用来标记线程是获取独资源时被挂起后放入AQS中的,waitStatus记录当前线程等待状态,可以为CANCELLED(线程被取消了)SINGNAL, (线程需要等待被唤醒)CONDITION(线程在条件队列里等待)PROPAGATE(释放共享资源时需要通知其他节点)prev记录当前节点的前驱节点,next记录当前节点的后继节点。在AQS 中维持了一个单一的状态信息state,可以通过getState 、setState 、compareAndS etState 函数修改其值。对于Reentran tLock 的实现来说, state 可以用来表示当前线程获取锁的可重入次数;对于读写锁ReentrantReadWri teLock 来说, state 的高16位表示读状态,也就是获取该读锁的次数低16 位表示获取到写锁的线程的可重入次数对于semaphore 来说, state 用来表示当前可用信号的个数:对于CountDownlatch 来说,state 用来表示计数器当前的值。AQS 有个内部类ConditionObject , 用来结合锁实现线程同步。ConditionObject 可以直接访问AQS 对象内部的变量,比如state 状态值和AQS 队列。C onditionObject 是条件变量, 每个条件变量对应一个条件队列(单向链表队列),其用来存放调用条件变量的await 方法后被阻塞的线程。

对于AQS 来说,线程同步的关键是对状态值state 进行操作。根据state (线程状态信息)是否属于一个线程,操作state 的方式分为独占方式和共享方式。如独占锁ReentrantLock 的实现,

  • (独占方式) 当一个线程获取了ReetrantLock 的锁后,在AQS 内部会首先使用CA S 操作把state 状态值从0变为1 ,然后设置当前锁的持有者为当前线程,当该线程再次获取锁时发现它就是锁的持有者,则会把状态值从l 变为2 ,也就是设置可重入次数,而当另外一个线程获取锁时发现自己并不是该锁的持有者就会被放入AQS 阻塞队列后挂起。
  • (共享方式)比如Semaphore 信号量, 当一个线程通过acquire() 方法获取信号量时,会首先看当前信号量个数是否满足需要, 不满足则把当前线程放入阻塞队列,如果满足则通过自旋CAS 获取信号量。

AQS独占方式下获取资释放源流程

当一个线程调用acquire(int tag)方法获取独占资源时,会首先使用tryAcquire方法尝试获取资源。

Created with Raphaël 2.3.0 acquire(int arg) tryAcquire(arg) 成功? 结束 将当前线程封装为类型为Node. EXCLUSIVE 的Node 节点后插入到AQS 阻塞队列的尾部,并调用 LockSupport. park( this) 方法挂起自己。 yes no
    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

当一个线程调用release(int arg)方法时会尝试使用tryRelease 操作释放资源,这里
是设置状态变量state 的值,然后调用LockSupport.unpark(thread)方法激活AQS 队列里面
被阻塞的一个线程(thread) 。被激活的线程则使用tryAcquire 尝试,看当前状态变量state
的值是否能满足自己的需要,满足则该线程被激活,然后继续向下运行,否则还是会被放
入AQ S 队列并被挂起。

    public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

附录: withsatus说明


        /** waitStatus value to indicate thread has cancelled */
        static final int CANCELLED =  1;
        /** waitStatus value to indicate successor's thread needs unparking */
        static final int SIGNAL    = -1;
        /** waitStatus value to indicate thread is waiting on condition */
        static final int CONDITION = -2;
        /**
         * waitStatus value to indicate the next acquireShared should
         * unconditionally propagate
         */
        static final int PROPAGATE = -3;
//  It will not be used as a sync queue node until transferred, at which time the status will be set to 0
/**
0: None of the above The values are arranged numerically to simplify use.
Non-negative values mean that a node doesn't need to signal. So, most code doesn't need to check for particular values, just for sign. The field is initialized to 0 for normal sync nodes,
*/

需要注意的是, AQS 类并没有提供可用的tryAcquire 和tryRelease 方法,正如AQS是锁阻塞和同步器的基础框架一样, tyAcquire 和tryRelease 需要由具体的子类来实现。子类在实现tryAcquire 和tryRelease 时要根据具体场景使用CAS 算法尝试修改state 状态值,成功则返回true,否则返回false 。子类还需要定义,在调用acquire 和release 方法时state状态值的增减代表什么含义。

在这里插入图片描述
AQS结构

AQS共享方式下获取资释放源流程

  • 获取
    会首先使用trγAcq山reS hared
    尝试获取资源, 具体是设置状态变量state 的值,成功则直接返回,失败则将当前线程封装为类型为Node . SHARED 的Node 节点后插入到AQS 阻塞队列的尾部,并使用LockSupport. park( th is) 方法挂起自己。
    public final void acquireShared(int var1) {
        if (this.tryAcquireShared(var1) < 0) { // <0 获取失败
            this.doAcquireShared(var1);
        }

    }

doAcquiredShared(var1)[有点迷糊… ,似乎并不是单纯的直接挂起,还是会有一个判断,似乎是用来判断当前阻塞队列中是否仅有本元素?


    /**
     * Acquires in shared uninterruptible mode.
     * @param arg the acquire argument
     */
    private void doAcquireShared(int arg) {
        final Node node = addWaiter(Node.SHARED);
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                if (p == head) {
                    int r = tryAcquireShared(arg);
                    if (r >= 0) {
                        setHeadAndPropagate(node, r);
                        p.next = null; // help GC
                        if (interrupted)
                            selfInterrupt();
                        failed = false;
                        return;
                    }
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

tryAcquireShared 和tryRelease Shared方法同样需要子类去自己实现。

releaseShared

    public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) {
            doReleaseShared();
            return true;
        }
        return false;
    }

relase


    /**
     * Release action for shared mode -- signals successor and ensures
     * propagation. (Note: For exclusive mode, release just amounts
     * to calling unparkSuccessor of head if it needs signal.)
     */
    private void doReleaseShared() {
        /*
         * Ensure that a release propagates, even if there are other
         * in-progress acquires/releases.  This proceeds in the usual
         * way of trying to unparkSuccessor of head if it needs
         * signal. But if it does not, status is set to PROPAGATE to
         * ensure that upon release, propagation continues.
         * Additionally, we must loop in case a new node is added
         * while we are doing this. Also, unlike other uses of
         * unparkSuccessor, we need to know if CAS to reset status
         * fails, if so rechecking.
         */
        for (;;) {
            Node h = head;
            if (h != null && h != tail) {
                int ws = h.waitStatus;
                if (ws == Node.SIGNAL) {
                    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                        continue;            // loop to recheck cases
                    unparkSuccessor(h);
                }
                else if (ws == 0 &&
                         !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                    continue;                // loop on failed CAS
            }
            if (h == head)                   // loop if head changed
                break;
        }
    }

当一个线程调用releaseShared(int a电)时会尝试使用tryReleaseS hared 操作释放资源,这里是设置状态变量state 的值,然后使用LockSupport.unpark ( thread )激活AQS 队列里面被阻塞的一个线程(thread) 。被激活的线程则使用tryReleaseS hared 查看当前状态变量state 的值是否能满足自己的需要,满足则该线程被撤活,然后继续向下运行,否则还是会被放入AQS 队列并被挂起。

读写锁实例tryAcquireShared过程

Created with Raphaël 2.3.0 开始 写锁被占用? 使用cas递增高16位 结束 false yes no

Interruptibly

    public final void acquireSharedInterruptibly(int arg)
            throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
        if (tryAcquireShared(arg) < 0)
            doAcquireSharedInterruptibly(arg);
    }

可以看到类中对于acquire和acquireShared方法都有对应的Interruputibly方法, 不带Intenuptibly 关键字的方法的意思是不对中断进行响应忽略中断,带InterruptedException关键字的方法需要对中断进行相应,,也就是线程在调用带Interruptibly关键字的方法获取资源时或者获取资源失败被挂起时,其他线程中断了该线程,那么该线程会抛出InterruptedException 异常而返回。

入队操作

当一个线程获取锁失败后该线程会被转换为Node 节点,然后就会使用,enq(final Node node) 方法将该节点插入到AQS 的阻塞队列。

    /**
     * Inserts node into queue, initializing if necessary. See picture above.
     * @param node the node to insert
     * @return node's predecessor
     */
    private Node enq(final Node node) {
        for (;;) {
            Node t = tail;
            if (t == null) { // Must initialize
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

代码最外层被死循环包裹着,在进行插入操作的时候,考虑到以下几种情况

  • 尾节点指向的是空,实际上头节点也指向的是空,队中现在没有节点,此时需要在队中插入一个哨兵节点,插入后再进行下一次循环,进行尾插的工作,在插入的过程中使用cas插入,如果失败了,会不停的进行自旋。
    *在这里插入图片描述

条件变量的支持

signal代码

        public final void signal() {
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
            Node first = firstWaiter;
            if (first != null)
                doSignal(first);
        }
        private void doSignal(Node first) {
            do {
                if ( (firstWaiter = first.nextWaiter) == null)
                    lastWaiter = null;
                first.nextWaiter = null;
            } while (!transferForSignal(first) &&
                     (first = firstWaiter) != null);
        }

await代码

        public final void await() throws InterruptedException {
            if (Thread.interrupted())
                throw new InterruptedException();
            Node node = addConditionWaiter();
            int savedState = fullyRelease(node);
            int interruptMode = 0;
            while (!isOnSyncQueue(node)) {
                LockSupport.park(this);
                if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                    break;
            }
            if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
                interruptMode = REINTERRUPT;
            if (node.nextWaiter != null) // clean up if cancelled
                unlinkCancelledWaiters();
            if (interruptMode != 0)
                reportInterruptAfterWait(interruptMode);
        }

notify 和wait ,是配合synchronized 内置锁实现线程间同步的基础设施一样条件变量的s i gnal 和await 方法是用来配合锁(使用AQ S 实现的锁〉实现线程间同步的基础设施。synchronized 同时只能与一个共享变量的notify 或wait 方法实现同步,而AQ S 的一个锁可以对应多个条件变量。

条件变量

调用条件变量的await()方法就相当于调用共享变量的wait()方法,调用条件变量的s ignal 方法就相当于调用共享变量的notify() 方法。调用条件变量的signa!All ( )方法就相当于调用共享变量的notifyAll ()方法。
在这里插入图片描述

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值