AQS详解

第一章 AQS简介
1.1 简要介绍
AQS,全称是 AbstractQueuedSynchronizer,中文译为抽象队列式同步器。

AQS 中有两个重要的东西,一个以Node为节点实现的链表的队列,还有一个用volatile修饰的STATE标志,并且通过CAS来改变它的值。

1.1.1 state标志
State的话一般用来标记一些状态,如在ReetrantLock中State用来标记是否有线程占有锁与重入次数,在CountLatchDown中用来标记占有锁的线程个数,这完全取决于用户。

AQS提供了三个方法来操作state

getState()
setState()
compareAndSetState()
1.1.2 队列
队列的话有以下特点:

链表结构,在头尾结点中,需要特别指出的是头结点是一个空对象结点,无任何意义,即傀儡结点;

每一个Node结点都维护了一个指向前驱的指针和指向后驱的指针,结点与结点之间相互关联构成链表;

入队在尾,出队在头,出队后需要激活该出队结点的后继结点,若后继结点为空或后继结点waitStatus>0,则从队尾向前遍历取waitStatus<0的触发阻塞唤醒;

队列中节点状态值(waitStatus,只能为以下值)

//常量:表示节点的线程是已被取消的
static final int CANCELLED =  1;
//常量:表示当前节点的后继节点的线程需要被唤醒
static final int SIGNAL    = -1;
//常量:表示线程正在等待某个条件
static final int CONDITION = -2;
//常量:表示下一个共享模式的节点应该无条件的传播下去
static final int PROPAGATE = -3;
1.2 支持的模式
AQS支持线程抢占两种锁——独占锁和共享锁:

独占锁:同一个时刻只能被一个线程占有,如ReentrantLock,ReentrantWriteLock等,它又可分为:
公平锁:按照线程在队列中的排队顺序,先到者先拿到锁
非公平锁:当线程要获取锁时,无视队列顺序直接去抢锁,谁抢到就是谁的
共享锁:同一时间点可以被多个线程同时占有,如ReentrantReadLock,Semaphore等
AQS的所有子类中,要么使用了它的独占锁,要么使用了它的共享锁,不会同时使用它的两个锁。

1.3 需要用户实现的部分
isHeldExclusively():该线程是否正在独占资源。只有用到condition才需要去实现它。
tryAcquire(int):独占方式。尝试获取资源,成功则返回true,失败则返回false。
tryRelease(int):独占方式。尝试释放资源,成功则返回true,失败则返回false。
tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
tryReleaseShared(int):共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false。

第二章 AQS独占锁执行流程
2.1 aquire
acquire是用来获取资源的,当我们自己实现的tryAcquire中返回true,即获取资源成功后,便不会执行多余的操作,会执行直接跳出,当获取资源失败后,会调用
我们把acquireQueued(addWaiter(Node.EXCLUSIVE), arg)拆解来看,对于addWaiter(Node.EXCLUSIVE)来说,如果尾部节点不是空的,就把新的线程封装成节点加入到尾部节点中,如果尾部节点是空的,说明当前队列是空的,则调用enq方法

 

private Node addWaiter(Node mode) {
        Node node = new Node(Thread.currentThread(), mode);
        // Try the fast path of enq; backup to full enq on failure
        Node pred = tail;
        if (pred != null) {
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        enq(node);
        return node;
    }


在enq方法中先建立头节点,再采用CAS方法把当前节点添加到尾部节点。

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;
                }
            }
        }
    }

再之后我们分析acquireQueued方法,先判断当前直接节点的前驱节点是不是头节点,如果是头节点,那么说明这个节点是队列中第一个线程,那就再尝试获取锁,如果能获取锁,那就直接把当前线程当作当前线程,如果不能获取线程,那就调用shouldParkAfterFailedAcquire判断下这个节点能否被正常唤醒,如果能被正常,那么调用parkAndCheckInterrupt阻塞当前线程,release的阻塞到这里就结束了。

最后我们看一下它是如何判断能不能被正常唤醒的,当前置节点的SIGNAL等-1,会直接返回true,如果大于0,会删掉一直向前走,删掉SIGNAL大于0的节点,如果是其它值(如默认的0),则会把前置节点SIGNAL置为-1,后面两种SIGNAL的情况还会继续再循环一遍。

2.2 release
首先判断调用tryRelease判断能不能释放,如果能释放的话将头节点传入unparkSuccessor()中

public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }
如果头节点状态是-1,修改头节点状态为0,找到头节点的下一个节点,如果下一个是空或者状态大于0(因为多线程吗,可能在增加删除节点时暂时出现这种情况),则从队尾开始找,找到第一个状态是-1的节点,然后唤醒。

  

这里是本文最重要的:

当线程被LockSupport.unpark(s.thread)唤醒后,不是说它就会从release的地方去执行了,而是会到我们阻塞线程的地方去执行,也就是下面的代码处,它会从parkAndCheckInterrupt()后面继续执行,重新进入for循环,再次判断能不能获取(值得注意的是上面我们说到它会从后向前遍历,而这里还是增加了前面得节点是不是头结点的判断,所以本人推测从前或者从后遍历其实遍历的都是头节点的下一个节点)
————————————————
版权声明:本文为CSDN博主「逆袭的小学生」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/q610376681/article/details/108917792

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值