AQS源码分析

30 阅读9分钟

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.predecessor();
            //如果前驱是head结点,即该结点是第二个结点并且能够获取到资源
            if (p == head && tryAcquire(arg)) { 
                setHead(node);//将当前结点设置为头结点
                p.next = null; // help GC
                return interrupted;
            }
            //如果可以进入等待状态,则返回true,进入方法内部,该if条件里面的方法是去寻找它前面最近
            //的一个WaitStatus为Signal状态的结点,并清理掉那些状态为Cancel的结点和将其余状态的
            //结点的waitStatus修改为Signal。从而这里一般会进行两次判断,实现了自旋。
            if (shouldParkAfterFailedAcquire(p, node))  
                interrupted |= parkAndCheckInterrupt();
        }
    } catch (Throwable t) {
        cancelAcquire(node);
        if (interrupted)
            selfInterrupt();
        throw t;
    }
}
shouldParkAfterFailedAcquire(Node, Node)

此方法主要用于检查状态,看看自己是否真的可以将自己挂起(进入waiting状态,如果线程状态转换不熟,可以参考Thread详解

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    //获取前驱结点的状态
    int ws = pred.waitStatus;
    if (ws == Node.SIGNAL) //前驱结点在释放时会通知当前结点,当前结点可以安全地进入等待状态
        return true;
    if (ws > 0) { 
        do {  //前驱结点被取消,此时需要移除这些结点
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        //如果前驱结点正常(必须为0或者PROPAGATE),表明我们需要信号,故将前驱的状态改为SIGNAL状
        //态
        pred.compareAndSetWaitStatus(ws, Node.SIGNAL);
    }
    return false;
}
parkAndCheckInterrupt()

如果线程找好修改好前驱节点的状态值后,就可以将自己挂起

private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);
        return Thread.interrupted();  
    }

park()会让当前线程进入waiting状态。在此状态下,有两种途径可以唤醒该线程:1)被unpark();2)被interrupt()。

acquire()小结

源码:

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

流程:

  1. 调用子类的的tryAcquire()尝试直接去获取资源,如果成功则直接返回;
  2. 如果失败,则addWaiter()以独占模式将该线程加入等待队列的尾部;
  3. acquireQueued()使线程在CLH队列中等待,同时(轮到自己,或被unpark())会去尝试获取资源。获取到资源后才返回。如果在整个等待过程中被中断过,则返回true,否则返回false。
  4. 如果线程在等待过程中被中断过,它是不响应的。只是获取资源后才再进行自我中断selfInterrupt(),将中断补上。 image.png

release(int)

release方法是独占模式下线程释放共享资源的顶层入口。他会释放指定量的资源,如果彻底释放了,它会唤醒等待队列里的其他线程来获取资源。值得注意的是:release()方法是根据tryRelease()方法的返回值来判断该线程是否已经完成资源释放了,同步器在设计tryRelease()的时候要明确这一点!!

public final boolean release(int arg) {
    if (tryRelease(arg)) {
    Node h = head; //找到头结点
    if (h != null && h.waitStatus != 0)
        unparkSuccessor(h); //唤醒等待队列里的下一个线程
    return true;
}
return false;
}
tryRelease(int)

跟tryAcquire()一样,这个方法是需要独占模式的自定义同步器去实现的。正常来说,tryReleae()方法都会成功的,因为这是独占模式,该线程来释放资源,那么它肯定已经拿到独占资源了,直接减掉相应量的资源即可(state-=arg),也不需要考虑线程安全的问题。但需要注意它的返回值,release()是根据tryRelease()的返回值来判断该线程时候已经完成资源释放的,所以在自定义同步器实现时,如果资源彻底释放(state=0),返回true,否则返回false。

protected boolean tryRelease(int arg) {
        throw new UnsupportedOperationException();
}
unparkSuccessor(Node)

此方法用于唤醒等待队列中最前边的那个未放弃的线程。下面是源码:

private void unparkSuccessor(Node node) {
        int ws = node.waitStatus; //获取当前线程所在的结点的状态
        if (ws < 0)
            node.compareAndSetWaitStatus(ws, 0);
    	
        Node s = node.next; //找到一个需要唤醒的结点s
        if (s == null || s.waitStatus > 0) {  //如果为空或已取消
            s = null;
            //从后往前找到队列中第一个未放弃的线程,这里从后往前寻找主要是由于addWaiter方法造成的
            //在addWaiter方法中后继指向前驱的结点是由CAS操作保证线程安全的,而CAS操作之后
            //oldtail.next = node之前,可能会有其他线程进来,因此从后往前找可以保证一定能遍历所有结
            //点
            for (Node p = tail; p != node && p != null; p = p.prev) 
                if (p.waitStatus <= 0)  
                    s = p;
        }
        if (s != null)
            LockSupport.unpark(s.thread);//唤醒
    }

和acquireQueued()联系起来,s被唤醒后,进入if(p==head&&tryAcquire(arg))的判断(即使p!=head也没关系,它会进入shouldParkAfterFailedAcquire()寻找一个安全点,这里既然s已经是等待队列中最前边的那个线程了,就会将s前边的失效结点全部给删除,下次自旋p==head就成立了)

release()小结

release()是独占模式下线程释放共享资源的顶层入口。它会释放指定量的资源,如果彻底释放了共享资源,则它会唤醒等待队列里的其他线程

acquireShared()

流程:

  1. tryAcquireShared()尝试获取资源,成功则直接返回
  2. 失败则通过doAcquireShared()方法进入等待队列,直到获取资源成功为止

该方法是共享模式下线程获取共享资源的顶层入口,它会获取指定量的资源,获取成功则直接返回,获取失败则进入等待队列,直到获取成功,整个过程中断忽略,源码如下:

public final void acquireShared(int arg) {
    if (tryAcquireShared(arg) < 0) //负值代表获取失败
    doAcquireShared(arg);
}
doAcquireShared(int)

此方法用于将当前线程加入到等待队列尾部休息,直到其他线程释放资源唤醒自己,成功拿到资源后才返回,源码如下:

private void doAcquireShared(int arg) {
    //加入等待队列队尾
    final Node node = addWaiter(Node.SHARED);
//等待过程中是否出现过中断
boolean interrupted = false;
try {
    for (;;) {
        final Node p = node.predecessor();
        if (p == head) { 
            int r = tryAcquireShared(arg); //尝试获取资源
            if (r >= 0) {
                setHeadAndPropagate(node, r); //将head指向自己,还有剩余资源可以再唤醒之后的线程
                p.next = null; // help GC
                return;
            }
        }
        if (shouldParkAfterFailedAcquire(p, node)) //将当前线程连接到队列中最后一个处于有效等待状态的线程的后面
            interrupted |= parkAndCheckInterrupt();
    }
} catch (Throwable t) {
    cancelAcquire(node);
    throw t;
} finally {
    //补中断
    if (interrupted)
        selfInterrupt();
}
}
setHeadAndPropagate(Node, int)

源码如下:

private void setHeadAndPropagate(Node node, int propagate) {
    Node h = head; // Record old head for check below
    setHead(node); //head指向自己
    //如果还有剩余量,继续唤醒下一个线程
    if (propagate > 0 || h == null || h.waitStatus < 0 ||
        (h = head) == null || h.waitStatus < 0) {
        Node s = node.next;
        if (s == null || s.isShared())
            doReleaseShared();
    }
}

acquireShared()小结

该方法跟acquire()的流程大同小异,只不过多了个自己拿到资源后,还会去唤醒后继队友的操作 流程:

  1. tryAcquireShared()尝试获取资源,成功则直接返回;
  2. 失败则通过doAcquireShared()进入等待队列park(),直到被unpark()/interrupt()并成功获取到资源才返回。整个等待过程也是忽略中断的。

releaseShared()

此方法是共享模式下线程共享资源的顶层入口。它会释放指定量的资源,如果成功释放且允许唤醒等待线程,它会唤醒等待队列里的其他线程来获取资源,源码如下:

public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) {  //尝试释放资源
    doReleaseShared(); //唤醒后继结点
    return true;
}
return false;
}

此方法的流程也比较简单,一句话:释放掉资源后,唤醒后继。跟独占模式下的release()相似,但有一点稍微需要注意:独占模式下的tryRelease()在完全释放掉资源(state=0)后,才会返回true去唤醒其他线程,这主要是基于独占下可重入的考量;而共享模式下的releaseShared()则没有这种要求,共享模式实质就是控制一定量的线程并发执行,那么拥有资源的线程在释放掉部分资源时就可以唤醒后继等待结点。例如,资源总量是13,A(5)和B(7)分别获取到资源并发运行,C(4)来时只剩1个资源就需要等待。A在运行过程中释放掉2个资源量,然后tryReleaseShared(2)返回true唤醒C,C一看只有3个仍不够继续等待;随后B又释放2个,tryReleaseShared(2)返回true唤醒C,C一看有5个够自己用了,然后C就可以跟A和B一起运行。而ReentrantReadWriteLock读锁的tryReleaseShared()只有在完全释放掉资源(state=0)才返回true,所以自定义同步器可以根据需要决定tryReleaseShared()的返回值。

doReleaseShared()

此方法主要用于唤醒后继,下面是它的源码:

private void doReleaseShared() {
        for (;;) {
            Node h = head;
            if (h != null && h != tail) {
                int ws = h.waitStatus;
                if (ws == Node.SIGNAL) { // 通知后继线程
                    if (!h.compareAndSetWaitStatus(Node.SIGNAL, 0))
                        continue;            // loop to recheck cases
                    unparkSuccessor(h);
                }
                else if (ws == 0 &&
                         !h.compareAndSetWaitStatus(0, Node.PROPAGATE))
                    continue;                // loop on failed CAS
            }
            if (h == head)                   // loop if head changed
                break;
        }
    }

Condition相关

使用示例

class BoundedBuffer {
    final Lock lock = new ReentrantLock();
    final Condition notFull  = lock.newCondition(); 
    final Condition notEmpty = lock.newCondition(); 

    final Object[] items = new Object[100];
    int putptr, takeptr, count;

    public void put(Object x) throws InterruptedException {
        lock.lock();
        try {
            while (count == items.length)
                notFull.await();
            items[putptr] = x;
            if (++putptr == items.length) putptr = 0;
            ++count;
            notEmpty.signal();
        } finally {
            lock.unlock();
        }
    }

    public Object take() throws InterruptedException {
        lock.lock();
        try {
            while (count == 0)
                notEmpty.await();
            Object x = items[takeptr];
            if (++takeptr == items.length) takeptr = 0;
            --count;
            notFull.signal();
            return x;
        } finally {
            lock.unlock();
        }
    }
}

Object 的监视器方法:wait、notify、notifyAll 应该都不陌生,在多线程使用场景下,必须先使用 synchronized 获取到锁,然后才可以调用 Object 的 wait、notify。 Condition 的使用,相当于用 Lock 替换了 synchronized,然后用 Condition 替换 Object 的监视器方法。

重点方法

await

await方法用于讲当前线程放入到条件等待队列中去,直到线程被唤醒或被中断,await方法整体流程如下:

  1. 创建 Node.CONDITION 类型的 Node 并添加到条件队列(ConditionQueue)的尾部;
  2. 释放当前线程获取的锁(通过操作 state 的值)
  3. 判断当前线程是否在同步队列(SyncQueue)中,不在的话会使用 park 挂起。
  4. 循环结束之后,说明已经已经在同步队列(SyncQueue)中了,后面等待获取到锁,继续执行即可。
public final void await() throws InterruptedException {
    // 响应中断
    if (Thread.interrupted())
        throw new InterruptedException();
    // 添加到条件队列尾部(等待队列)
    // 内部会创建 Node.CONDITION 类型的 Node
    Node node = addConditionWaiter();
    // 释放当前线程获取的锁(通过操作 state 的值)
    // 释放了锁就会被阻塞挂起
    int savedState = fullyRelease(node);
    int interruptMode = 0;
    // 节点已经不在同步队列中,则调用 park 让其在等待队列中挂着
    while (!isOnSyncQueue(node)) {
        // 调用 park 阻塞挂起当前线程
        LockSupport.park(this);
        // 说明 signal 被调用了或者线程被中断,校验下唤醒原因
        // 如果因为终端被唤醒,则跳出循环
        if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
            break;
    }
    // while 循环结束, 线程开始抢锁
    if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
        interruptMode = REINTERRUPT;
    if (node.nextWaiter != null) // clean up if cancelled
        unlinkCancelledWaiters();
    // 统一处理中断的
    if (interruptMode != 0)
        reportInterruptAfterWait(interruptMode);
}
signal
public final void signal() {
    // 是否为当前持有线程
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    Node first = firstWaiter;
    if (first != null)
        doSignal(first);
}

private void doSignal(Node first) {
    do {
        // firstWaiter 头节点指向条件队列头的下一个节点
        if ( (firstWaiter = first.nextWaiter) == null)
            lastWaiter = null;
        // 将原来的头节点和同步队列断开
        first.nextWaiter = null;
    } while (!transferForSignal(first) &&
                (first = firstWaiter) != null);
}

final boolean transferForSignal(Node node) {
 
    // 判断节点是否已经在之前被取消了
    if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
        return false;

    // 调用 enq 添加到 同步队列的尾部
    Node p = enq(node);
    int ws = p.waitStatus;
    // node 的上一个节点 修改为 SIGNAL 这样后续就可以唤醒自己了
    if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
        LockSupport.unpark(node.thread);
    return true;
}

小结

本文主要讲述了AQS在独占和共享模式下获取-释放资源(acquire-release、acquireShared-releaseShared)的源码,同时也简单介绍了Condition相关的使用方法和实现原理。值得注意的是:acquire和acquireShared方法中,线程在等待队列中都是忽略中断的。当然,AQS也支持响应中断,acquireInterruptibly()/acquireSharedInterruptibly()即可以响应中断(通过抛出异常的方式)。希望本文对您有所帮助,如有错误,还望不吝指正,感谢~


参考:

  1. Java并发之AQS详解
  2. 从ReentrantLock的实现看AQS的原理及应用
  3. AQS 都看完了,Condition 原理可不能少