AQS源码分析

image.png

概要

AQS,全称AbstractQueuedSynchronizer,抽象队列同步器,是Java多线程中的一个基础类,为诸多多线程工具类(如CountDownLatch,CyclicBarrie、ReentrantLock等)提供了基础框架,本文主要对AQS内部实现做了比较深入的分析

数据模型

Node节点

Node类是AQS中实现的一个内部类,用于包装线程以及线程状态的表示,其包含了需要同步的线程本身及其等待状态,如是否被阻塞、是否等待唤醒、是否已经被取消等。变量waitStatus则表示当前Node结点的等待状态,共有5种取值CANCELLED、SIGNAL、CONDITION、PROPAGATE、0。

  • CANCELLED(1):表示当前结点已取消调度。当timeout或被中断(响应中断的情况下),会触发变更为此状态,进入该状态后的结点将不会再变化。
  • SIGNAL(-1):表示后继结点在等待当前结点唤醒。后继结点入队时,会将前继结点的状态更新为SIGNAL。
  • CONDITION(-2):表示结点等待在Condition上,当其他线程调用了Condition的signal()方法后,CONDITION状态的结点将从等待队列转移到同步队列中,等待获取同步锁。
  • PROPAGATE(-3):共享模式下,前继结点不仅会唤醒其后继结点,同时也可能会唤醒后继的后继结点。
  • 0:新结点入队时的默认状态。

注意,负值表示结点处于有效等待状态,而正值表示结点已被取消。所以源码中很多地方用>0、<0来判断结点的状态是否正常

static final class Node {
   

    static final Node SHARED = new Node();

    static final Node EXCLUSIVE = null;


    static final int CANCELLED =  1;

    static final int SIGNAL    = -1;

    static final int CONDITION = -2;

    static final int PROPAGATE = -3;


    volatile int waitStatus;

    volatile Node prev;

    volatile Node next;

    volatile Thread thread;

    Node nextWaiter;
    // ...
}

CLH队列

CLH队列是AQS里面维护的一个双向链表,链表的每个节点是一个Node对象,抢占锁失败的线程就会加入到该链表中
image.png

ConditionObject

ConditionObject实现了Condition接口,主要用于AQS中的条件等待,每个ConditionObject都会维护一个链表,其中节点也是上述的Node示例,用于表示处在当前条件等待队列中的线程,但不同于CLH队列的是,这里的链表只是一个单向链表

重点方法分析

acquire(int)

方法流程如下:

  1. tryAcquire方法(该方法在AQS中默认抛出异常,需要子类根据自己的需求重写此方法)尝试直接去获取资源,如果成功则直接返回(这里体现了非公平锁,每个线程获取锁时会尝试直接抢占加塞一次,而CLH队列中可能还有别的线程在等待)
  2. addWaiter方法将该线程加入等待队列的尾部,并标记为独占模式;
  3. acquireQueued方法使线程阻塞在等待队列中获取资源,一直获取到资源后才返回。如果在整个等待过程中被中断过,则返回true,否则返回false。
  4. 如果线程在等待过程中被中断过,它是不响应的。只是获取资源后才再进行自我中断selfInterrupt(),将中断补上。

此方法是独占模式下线程获取共享资源的顶层入口。如果获取到资源,线程直接返回,否则进入等待队列,直到获取到资源为止,且整个过程忽略中断的影响。这也正是lock()的语义,当然不仅仅只限于lock()。获取到资源后,线程就可以去执行其临界区代码了。下面是acquire()的源码:

public final void acquire(int arg) {
   
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}
tryAcquire(int)

尝试去获取独占资源,如果获取成功,则直接返回true,否则直接返回false。

//默认抛出异常,具体实现交由具体的同步器根据业务需求去实现
protected boolean tryAcquire(int arg) {
   
    throw new UnsupportedOperationException();
}
addWaiter(Node)
private Node addWaiter(Node mode) {
   
    Node node = new Node(mode);

for (;;) {
    //自旋
    //获得尾结点
    Node oldTail = tail;
    if (oldTail != null) {
     //如果尾节点不为空说明现在队列已初始化,直接放入到队尾
        node.setPrevRelaxed(oldTail);
        if (compareAndSetTail(oldTail, node)) {
    //通过cas将尾节点修改为node
            oldTail.next = node;
            return node;
        }
    } else {
   //如果尾节点为空说明队列中没有结点,需要初始化
        initializeSyncQueue();
    }
}
}
acquireQueued(Node,int)

流程:

  1. 结点进入队尾,查看自己的前驱节点是否是头节点,如果是,则再次尝试获取锁
  2. 从后往前查看当前CLH队列中的节点,直到找到节点的waitStatus<=0才结束,这里waitStatus>0表示该线程取消,需要清理掉这些节点
  3. 调用park进入waitting状态,等待unpark()或interrupt()唤醒自己
  4. 被唤醒后,查看是否可以获取资源,如果拿到,head指向当前结点,并返回从入队拿到号的整个过程中是否被中断过;如果没拿到,继续流程1.

通过tryAcquire()和addWaiter(),该线程获取资源失败,已经被放入到等待队列尾部了。此时线程需要将自己挂起,直到其他线程释放资源后唤醒自己,自己拿到资源后,再去执行临界区代码
自旋:这里这个循环一般来说都会执行两次
第一次,线程进入shouldParkAfterFailedAcquire会去寻找第一个可用的前驱节点并修改该节点的waitStatus为SIGNAL(用来后面唤醒自己)并清除状态为Cancel的节点。
第二次,再次进入shouldParkAfterFailedAcquire(),此时当前节点的前驱节点的状态已经被修改为SIGNAL,函数返回true,然后执行parkAndCheckInterrupt将自己挂起。

final boolean acquireQueued(final Node node, int arg) {
   
    boolean interrupted = false;  //标记是否被中断过
    try {
   
        //自旋!!
        for (;;) {
   
            //拿到前驱结点
            final Node p = node
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值