深扒AQS(一)之独占共享解析

mac2025-11-27  13

AQS可以说是并发中最关键的一环了,包括我们前边用过了无数次的ReentrantLock以及各种锁就是AQS的典型应用。

AQS全名为AbstractQueuedSynchronizer,抽象同步队列

AQS属性

private transient volatile Node head; private transient volatile Node tail; private volatile int state; 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; } } public class ConditionObject implements Condition, java.io.Serializable { private transient Node firstWaiter; private transient Node lastWaiter; }

可以看到队列元素类型是Node,Node内部有前驱和后驱节点,所以其实队列内部是由一个双向链表组成,想一下我们这个队列的用处,是用来同步线程间的状态的,所以Node内部有一个Thread变量,节点有两种类型

SHARED 表示该节点对应线程是获取共享资源被阻塞挂起放入队列EXCLUSIVE 与SHARED相反,获取独占资源被阻塞挂起放入队列

state,比如说ReentrantLock,这是可重入锁,这里的state指的是可重入次数,当然,针对不同的应用,state有着不同的语义,后边我们会详细说。

还有一个属性是waitStatus,表示线程的等待状态,初始化为阻塞队列节点时默认为0,有以下四种

CANCELLED 当前节点线程由于超时或中断被取消 SIGNAL 当前节点的后驱节点被或即将被park,需要被当前节点线程unpark CONDITION 线程在条件队列中等待,当从阻塞队列中转移到条件队列时变为0 PROPAGATE 释放共享资源时通知其他节点

最后还有一个ConditionObject,这里先不着急,你只要知道有这么个东西就好啦

上边说了资源分为共享和独占状态,不同的资源对应的Api不一样,就从独占看起吧

独占式

获取资源 acquire(int arg)

public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); } protected boolean tryAcquire(int arg) { throw new UnsupportedOperationException(); } private Node addWaiter(Node mode) { // 将当前线程封装为mode指定类型节点 Node node = new Node(Thread.currentThread(), mode); Node pred = tail; // 尾节点不为空的话,cas将当前节点设为尾节点,cas和volatile结合保证了添加的有序性 if (pred != null) { node.prev = pred; if (compareAndSetTail(pred, node)) { pred.next = node; return node; } } // 尾节点为空OR上边cas失败,无限循环cas设置尾节点 enq(node); return node; } final boolean acquireQueued(final Node node, int arg) { boolean failed = true; try { boolean interrupted = false; for (;;) { // p是node的前驱节点 final Node p = node.predecessor(); // 当前驱节点是头节点时才能获取资源,成功后将将node设为头节点 if (p == head && tryAcquire(arg)) { setHead(node); p.next = null; // help GC failed = false; return interrupted; } // 这里当node的前驱结点状态是signal时返回true,本次是false的话下次for循环过来会变为true,接着park node,如果当前线程被中断,设置interrupted = true,下次for过来继续park;如果是被正常unpark,就不用管了 if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) interrupted = true; } } finally { if (failed) cancelAcquire(node); } } // 只有pred节点的状态是signal返回true;否则的话cas设置为signal返回false 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 { // 每次for都会来尝试设置前驱节点状态为signal compareAndSetWaitStatus(pred, ws, Node.SIGNAL); } return false; } // 挂起当前线程,最后返回当前线程是否被中断,会清除中断位 private final boolean parkAndCheckInterrupt() { LockSupport.park(this); return Thread.interrupted(); }

说实话,这里本来真的不想详细写了,良心不安,觉得有点不认真,也罢,我们就一行一行分析吧。

这个方法首先尝试获取资源,成功的话,if就不会再执行后边的了;获取失败的话,说明其他线程持有资源,将当前线程 封装为类型为 Node.EXCLUSIVE 的 Node 节点后插入到 AQS 阻塞 队列的尾部,设置前驱节点状态为SIGNAL,并park挂起自己加入队列后,当前节点在死循环中尝试获取同步状态,只有头节点是前驱节点时,该节点才能尝试获取资源,获取到资源后把自己设为头节点,即头节点总是获取到资源的节点

分割线~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 关于enq方法,本来没打算细写,想着无非就将节点插入到队列最后边,后来发现对节点关系不是很清楚,还是来看下吧

private Node enq(final Node node) { for (;;) { Node t = tail; if (t == null) { // 此时链表没有节点,需要初始化让head跟tail都指向一个哨兵节点 if (compareAndSetHead(new Node())) tail = head; } else { // 正常插入尾部 node.prev = t; if (compareAndSetTail(t, node)) { t.next = node; return t; } } } }

所以事实上AQS中队列的头节点是个哨兵节点

释放资源

public final boolean release(int arg) { if (tryRelease(arg)) { Node h = head; // 如果头节点不为空且waitStatus不为CANCELLED if (h != null && h.waitStatus != 0) // 唤醒后面可用节点(不为空或waitStatus<0) unparkSuccessor(h); return true; } return false; } protected boolean tryRelease(int arg) { throw new UnsupportedOperationException(); } private void unparkSuccessor(Node node) { int ws = node.waitStatus; // 将node的waitStatus设置为0 if (ws < 0) compareAndSetWaitStatus(node, ws, 0); Node s = node.next; // 得到后面不为空且不为CANCELLED的节点并unpark唤醒 if (s == null || s.waitStatus > 0) { s = null; for (Node t = tail; t != null && t != node; t = t.prev) if (t.waitStatus <= 0) s = t; } if (s != null) LockSupport.unpark(s.thread); }

释放资源相比获取资源来的要简单一些,只需要unpark唤醒后面的可用节点即可,同时将头节点status设置为0.

独占式释放/获取总结

其实我们可以发现,释放和获取资源都是严格按照FIFO的,即通过链表的前后驱指针按顺序来的。

在获取资源时,节点每次都看自身前节点是否是头节点,若是就尝试获取资源;获取没成功不要紧,此时头节点状态是SIGNAL,此时该节点会使用LokSupport的park挂起自己,头节点释放资源后就会unpark该节点线程,下一轮循环中该节点就可以成功获取资源啦! 如果前节点不是头节点,那就继续自旋就好啦

关于共享式不做过多介绍了,如果有心思可以自己去读下,其实流程基本一样,简要说一下

共享式

共享获取资源

public final void acquireShared(int arg) { //尝试获取共享锁,返回值小于0表示获取失败 if (tryAcquireShared(arg) < 0) doAcquireShared(arg); } // 返回值<0获取锁失败;等于0获取共享锁成功,但它后续的线程是无法继续获取的,也就是 //不需要把它后面等待的节点唤醒;返回值大于0, //表示当前线程获取共享锁成功且需要把后续节点唤醒让它们去尝试获取共享锁 protected int tryAcquireShared(int arg) { throw new UnsupportedOperationException(); } 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) { // 注意这里的node是成功获取了共享锁的节点 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); } } private void setHeadAndPropagate(Node node, int propagate) { Node h = head; // 将当前获取到锁的节点设置为头节点 setHead(node); // propagate>0表示后边节点需要被唤醒; if (propagate > 0 || h == null || h.waitStatus < 0 || (h = head) == null || h.waitStatus < 0) { Node s = node.next; // 只要当前节点的后驱节点不是独占式,就进行唤醒 if (s == null || s.isShared()) doReleaseShared(); } } // 唤醒操作 private void doReleaseShared() { 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); } // 此时后驱节点不需要被唤醒,当前节点状态设置为PROPAGATE确保以后可以传递 else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) continue; } //观察头结点是否发生变化,没变表示设置完成,退出循环 //否则可能是其他线程获取到了锁,为了使自己的唤醒动作可以传递,必须进行重试(前边第一步的head会发生变化进行传递 if (h == head) break; } }

释放共享资源

public final boolean releaseShared(int arg) { //尝试释放共享锁 if (tryReleaseShared(arg)) { //唤醒过程,详情见上面分析 doReleaseShared(); return true; } return false; } 注意,释放了一个共享锁后,等待独占锁跟共享锁的线程都可以被唤醒进行尝试获取。

总结

不管是独占式还是共享式,都需要注意tryAcquire和tryRelease以及tryAcquireShared和tryReleaseShared在AQS中都是不提供实现的,可以看到只抛出了异常。实际上我们的AQS常常作为各种锁的内部类,这两个方法一般都是在锁中自定义实现的,具体的我们后边会详细讲解。

此外,在AQS中还提供了tryAcquireNanos以及acquireInterruptibly,顾名思义,前者提供超时时间,超过指定时间的话就放弃,后者则是响应中断,因为大体上还跟前边的一样,这里不再详细分析。

个人公众号:

最新回复(0)