Java并发编程之抽象同步队列AQS

248 阅读10分钟

这是我参与11月更文挑战的第23天,活动详情查看:2021最后一次更文挑战

简介

AbstractQueuedSyncronizer抽象同步队列简称AQS,它是实现同步器的基础组件,并发包中的锁的底层就是使用AQS实现的。

AQS是一个FIFO的双向队列,其内部通过节点head和tail记录队首和队尾元素,队列元素的类型为Node。其中Node中的Thread变量用来存放进入AQS队列里的线程;Node节点内部的SHARED用来标记该线程是获取共享资源是被挂起放入AQS队列的,EXCLUSIVE用来标记线程是获取独占资源时被挂起后放入AQS队列的;waitStatus记录当前线程等待状态(CANCELLED:线程被取消了,SIGNAL:线程需要被唤醒,CONDITION线程在条件队列里等待,PROPAGATE:释放共享资源时需要通知其他节点);prev记录当前节点的前驱结点,next记录当前节点的后继节点。

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;

    final boolean isShared() {
        return nextWaiter == SHARED;
    }


    final Node predecessor() throws NullPointerException {
        Node p = prev;
        if (p == null)
            throw new NullPointerException();
        else
            return p;
    }

在AQS中维持了一个单一的状态信息state,可以通过getState、setState、compareAndSetState函数修改其值。对于ReentrantLock的实现来说,state可以用来表示当前线程获取锁的重入次数;对于CountDownlatch来说,state用来表示计数器当前的值;对于读写锁ReentrantReadWriteLock来说,state的高16位表示读状态,也就是获取该读锁的次数,低16位表示获取到写锁的线程的可重入次数;对于semaphore来说,state用来表示当前可用信号的个数。

private volatile int state;

protected final int getState() {
    return state;
}


protected final void setState(int newState) {
    state = newState;
}

protected final boolean compareAndSetState(int expect, int update) {
    // See below for intrinsics setup to support this
    return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}

AQS有个内部类ConditionObject,用来结合锁实现线程同步,ConditionObject 可以直接访问 AQS 对象内部的变量,比如 state 状态值和 AQs 队列。 ConditionObject 是条件每个条件变量对应一个条件队列(单向链表队列),其用来存放调用条件变量的await方法后被阻塞的线程,如关图所示,这个条件队列的头、尾元素分别为 first Waiter和lastWaiter。对于AQS来说,线程同步的关键是对状态值state进行操作。根据state是否属于一个线程,操作state的方式分为独占方式和共享方式。

使用独占方式获取的资源是与具体线程绑定的,就是说如果一个线程获取到了资源,就会标记是这个线程获取到了,其他线程再尝试操作state获取资源时会发现当前该资源不是自己持有的,就会在获取失败后被阻塞。比如独占锁 ReentrantLock 的实现,当一个线程获取了 ReentrantLock 的锁后,在 AQS 内部会首先使用 CAS 操作把 state 状态值从0变为1,然后设置当前锁的持有者为当前线程,当该线程再次获取锁时发现它就是锁的持有者,则会把状态值从1变为2,也就是设置可重入次数,而当另外一个线程获取锁时发现自己并不是该锁的持有者就会被放入 AQS 阻塞队列后挂起。

对应共享方式的资源与具体线程是不相关的,当多个线程去请求资源时通过 CAS 方式竞争获取资源,当一个线程获取到了资源后,另外一个线程再次去获取时如果当前资源还能满足它的需要,则当前线程只需要使用 CAS 方式进行获取即可。比如 Semaphore 信专量,当一个线程通过acquire()方法获取信号量时,会首先看当前信号量个数是즘满足需要,不满足则把当前线程放入阻塞队列,如果满足则通过自旋 CAS 获取信号量。

在独占方式下,获取与释放资源的流程如下:

(1)当一个线程调用acquire(int arg)方法获取到独占资源时,会首先使用tryAcquire方法尝试获取资源,具体是设置状态变量的state的值,成功则直接返回,失败则将当前节点封装为Node.EXCLUSIVE的Node节点后插入到AQS阻塞队列的尾部,并调用LockSupport.park(this)方法挂起自己。

public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}
final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        //自旋
        for (;;) {
        //判断是当前节点的前置节点是否是head节点,
        //如果是说明该节点,可以开始去抢资源了(如果是第一个肯定就不用抢了),
        //因为正在占有资源了。
            final Node p = node.predecessor();
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

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

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

需要注意的是,AQS类并没有提供可用的tryRelease、tryAcqiure方法,它们需要由具体的子类来实现。

在共享方式下,获取与释放资源的流程如下:

(1) 当线程调用acquireShared(int arg)获取共享资源时,会首先使用tryAcqiureShared尝试获取资源,具体是设置状态变量state的值,成功则直接返回,失败则将当前线程封装为Node.SHARED的Node节点后插入到AQS阻塞队列的尾部,并使用LockSupport.park(this)方法挂起自己。

public final void acquireShared(int arg) {
    if (tryAcquireShared(arg) < 0)
        doAcquireShared(arg);
}

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

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

最后我们来看一下如何维护AQS提供的队列

  • 入队操作:当一个线程获取锁失败后该线程会被转换为Node节点,然后就会使用enq(final Node node)方法将该节点插入到AQS的阻塞队列。
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;
            }
        }
    }
}

如上面代码,如果队列中没有元素,也就是队列首尾指向null时,插入一个元素会使用CAS算法设置一个哨兵结点为头结点。这时候head和tail都指向哨兵节点。如果队列中有元素后,插入node节点,会将node的前向节点prev指向为当前的tail节点;然后通过CAS算法设置node节点为tail节点;最后将之前的尾节点的后驱节点设置为node。

AQS中条件变量的支持

如wait、notify是配合synchronized内置锁实现线程间同步的一样,条件变量的signal和await也是为了配合使用基于AQS实现的锁的线程间同步。

ReentrantLock lock = new ReentrantLock();
Condition condition = lock.newCondition();

lock.lock();
try {
    System.out.println("begin await");
    condition.await();
    System.out.println("end await");
}  catch (Exception e) {
    e.printStackTrace();
} finally {
    lock.unlock();
}

lock.lock();
try {
    System.out.println("begin signal");
    condition.signal();
    System.out.println("end signal");
}  catch (Exception e) {
    e.printStackTrace();
} finally {
    lock.unlock();
}

其实这里的 Lock 对象等价于 synchronized 加上共享变量,调用lock.lock()方法就用当于进入了synchronized块(获取了共享变量的内置锁),调用lock.unLock()方法就相当于退出synchronized块。调用条件变量的await()方法就相当于调用共享变量的wait()方法,调用条件变量的signal()方法就相当于调用共享变量的notify()方法。调用条件变量的signalAll()方法就相当于调用共享变量的notifyAll方法。

经过上面解释,相信大家已经知道条件变量是什么,它是用来做什么的了。在上面代码中, lock.new Condition()的作用其实是new了一个在 AQS 内部声明的ConditionObject 对象, ConditionObject 是 AQS 的内部类 可以访问 AQS 内部的变量(例如状态变量 state )和方法。在每个条件变量内部都维护了一个条件队列,用来存放调用条件变量的await()方法时被阻塞的线程。注意这个条件队列和 AQS 队列不是一回事。

在如下代码中,当线程调用条件变量的await()方法时(必须先调用锁的lock方法获取锁),在内部会构造一个类型为Node.CONDITION的node节点,然后将该节点插入条件队列末尾,之后当前线程会释放获取的锁(也就是会操作锁对应的 state 变量的值),并被阻塞挂起。这时候如果有其他线程调用lock.lock()尝试获取锁,就会有一个线程获取到锁,如果获取到锁的线程调用了条件变量的 await ()方法,则该线程也会被放入条件变量的阻塞队列,然后释放获取到的锁,在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);
}

在如下代码中,当另一个线程调用条件变量的signal方法时,在内部会把条件队列里面队头的一个线程节点从条件队列里面移除并放入AQS的阻塞队列里面,然后激活这个线程。

public final void signal() {
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    Node first = firstWaiter;
    if (first != null)
        doSignal(first);
}
private Node addConditionWaiter() {
    Node t = lastWaiter;
    // If lastWaiter is cancelled, clean out.
    if (t != null && t.waitStatus != Node.CONDITION) {
        unlinkCancelledWaiters();
        t = lastWaiter;
    }
    Node node = new Node(Thread.currentThread(), Node.CONDITION);
    if (t == null)
        firstWaiter = node;
    else
        t.nextWaiter = node;
    lastWaiter = node;
    return node;
}

注意:当多个线程同时调用lock.lock()方法获取锁时,只有一个线程获取到了锁,其他线程会被转换为Node节点插入到lock锁对应的AQS阻塞队列里面,并做自旋CAS尝试获取锁。如果获取到锁的线程又调用了对应的条件变量的await()方法,则该线程会释放获取到的锁,并被转换为Node节点插入到条件变量对应的条件队列里面。

这时候因为调用lock.lock()方法被阻塞到AQS队列里面的一个线程会获取到被释放的锁,如果该线程也调用了条件变量的await()方法则该线程也会被放入条件变量的条件队列里面。

当另外一个线程调用条件变量的signal()或者signalAll ()方法时,会把条件队列里面的一个或者全部Node节点移动到AQS的阻塞队列里面,等待时机获取锁。

最后,我们再看一下AQS总结的一个图:

image.png