AQS

AQS全称 AbstractQueuedSynchronizer,队列同步器,该组件是JUC包下的实现锁和其他同步组件的基础框架。我们先从JavaDoc看看是如何介绍的。因原文过长,这里直接理解摘取第一段,该段能大致了解该类的作用。若需要了解具体细节仍需要查看其他注释段落。翻译如下:

提供一个依赖先进先出(FIFO)等待队列实现阻塞锁和相关同步器的基础框架。该类被设计为有用的基础是因为大多数同步器都依赖于一个原子的int值来作为他们的状态。子类必须定义更改此状态的protected方法和状态获取和释放对该子类意味着什么。基于以上方法,该类中的其他方法则提供排队和阻塞的机制。子类还可以维护其他状态字段,但是需要使用 getState(), setState(int)compareAndSetState(int, int)来原子同步的更新int值。

简单总结一下:

  • AQS类
    • 提供FIFO队列及其排队和阻塞机制的方法
    • 提供一个int类型的值state来作为子类同步器的状态
  • 子类
    • 提供更改此状态的protected方法
    • 当实际需要获取或更改状态时需要使用对应方法

      state字段

      ```java /**
      • The synchronization state. */ private volatile int state;

//返回同步状态的当前值 protected final int getState() { return state; } //设置当前同步状态 protected final void setState(int newState) { state = newState; } //使用CAS设置当前状态,该方法能够保证状态设置的原子性 protected final boolean compareAndSetState(int expect, int update) { // See below for intrinsics setup to support this return unsafe.compareAndSwapInt(this, stateOffset, expect, update); }

  1. 该字段就是作为子类同步器的状态,volidate修饰该字段是为了保证多线程之间的可见性,具体在子类中意味着什么由实现子类决定。所以理解每个同步器类的`state` 字段都至关重要。比如在`ReentrantLock` 中该字段就表示当前线程持有了几次锁。
  2. <a name="OnH0D"></a>
  3. # FIFO等待队列
  4. 从介绍中看出AQS提供了FIFO等待队列,我们先看看AQS是如何提供FIFO等待队列及其对应的方法。队列中存储着什么呢?AQS类中首先定义了一个Node类作为队列的节点:节点存在2种模式,`SHARED``EXCLUSIVE`;存在5种状态用来控制线程的唤醒和阻塞:`CANCELLED``SIGNAL``CONDITION``PROPAGATE` ,`INITAL`;并且该节点为双向的,存在前驱节点和后续节点。(**其实每一个节点就是一个等待获取锁的线程**)。
  5. ```java
  6. public abstract class AbstractQueuedSynchronizer
  7. extends AbstractOwnableSynchronizer
  8. implements java.io.Serializable {
  9. static final class Node {
  10. /** 共享模式 */
  11. static final Node SHARED = new Node();
  12. /** 排他模式 */
  13. static final Node EXCLUSIVE = null;
  14. //节点等待状态
  15. // CANCELLED(1) :因为超时或者中断,节点会被设置为取消状态,被取消的节点时不会参与到竞争中的,他会一直保持取消状态不会转变为其他状态
  16. // SIGNAL (-1): 后继节点的线程处于等待状态,而当前节点的线程如果释放了同步状态或者被取消,将会通知后继节点,使后继节点的线程得以运行
  17. // CONDITION (-2): 节点在等待队列中,节点线程等待在Condition上,当其他线程对Condition调用了signal()后,该节点将会从等待队列中转移到同步队列中,加入到同步状态的获取中
  18. // PROPAGATE (-3): 表示下一次共享式同步状态获取将会无条件地传播下去
  19. volatile int waitStatus;
  20. //前驱节点
  21. volatile Node prev;
  22. //后续节点
  23. volatile Node next;
  24. //获取同步的线程
  25. volatile Thread thread;
  26. //等待队列中的后续节点
  27. Node nextWaiter;
  28. //返回是否为共享
  29. final boolean isShared() {
  30. return nextWaiter == SHARED;
  31. }
  32. //返回前继节点
  33. final Node predecessor() {
  34. Node p = prev;
  35. if (p == null)
  36. throw new NullPointerException();
  37. else
  38. return p;
  39. }
  40. Node() { // Used to establish initial head or SHARED marker
  41. }
  42. Node(Thread thread, Node mode) { // Used by addWaiter
  43. this.nextWaiter = mode;
  44. this.thread = thread;
  45. }
  46. Node(Thread thread, int waitStatus) { // Used by Condition
  47. this.waitStatus = waitStatus;
  48. this.thread = thread;
  49. }
  50. }
  51. //头节点
  52. private transient volatile Node head;
  53. //尾节点
  54. private transient volatile Node tail;
  55. }

加入队列

因为是FIFO队列,整个步骤是根据当前线程和传入的节点模式创建新节点并加入队列尾部。这里尾节点使用cas来操作就是保证尾节点是线程安全的。

  1. private Node addWaiter(Node mode) {
  2. //创建节点
  3. Node node = new Node(Thread.currentThread(), mode);
  4. // Try the fast path of enq; backup to full enq on failure
  5. //获取尾部节点
  6. Node pred = tail;
  7. if (pred != null) { //若队列中已有节点
  8. //则设置新节点的prev指向当前尾节点
  9. node.prev = pred;
  10. if (compareAndSetTail(pred, node)) { //若cas操作将新节点设置为尾节点
  11. //则将原来的尾节点的next指向新节点
  12. pred.next = node;
  13. return node;
  14. }
  15. }
  16. //循环尝试,直至成功
  17. enq(node);
  18. return node;
  19. }

enq方法通过一个死循环尝试,直到把新节点入队列。

  1. private Node enq(final Node node) {
  2. for (;;) {
  3. //获取尾节点
  4. Node t = tail;
  5. if (t == null) { // Must initialize 若为空队列
  6. if (compareAndSetHead(new Node())) //cas设置新节点为头节点
  7. tail = head; //尾节点也指向头结点
  8. } else { //非空队列 (操作同上)
  9. node.prev = t;
  10. if (compareAndSetTail(t, node)) {
  11. t.next = node;
  12. return t;
  13. }
  14. }
  15. }
  16. }

image.png
该图简单的展示了节点入队的过程。图中数字表示执行顺序。红色表示原先队列中的节点,而蓝色表示新增节点。

出队列

因为是FIFO队列,首节点在释放同步状态时将会通知后继节点,当后继节点得到同步状态时,会把自己设置为 首节点。这个过程中不需要是不需要cas操作的,因为只有一个线程,可以获得同步状态。

  1. /**
  2. * Sets head of queue to be node, thus dequeuing. Called only by
  3. * acquire methods. Also nulls out unused fields for sake of GC
  4. * and to suppress unnecessary signals and traversals.
  5. *
  6. * @param node the node
  7. */
  8. private void setHead(Node node) {
  9. head = node;
  10. node.thread = null;
  11. node.prev = null;
  12. }

过程图如图所示。
image.png

其他方法

AQS还定义了其他的方法来作为排队和阻塞的机制。AQS中定义了大量的模板方法来实现同步机制,由子类继承实现。主体上可以分为三大类:独占式获取、释放锁;共享式获取、释放锁;查看同步队列线程情况。

  1. 独占式:同一时刻只有一个线程可以获得锁
    • acquire(int arg):独占式获取同步状态,如果当前线程获取同步状态成功,则由该方法返回,否则,将会进入同步队列等待,该方法将会调用可重写的tryAcquire(int arg)方法;
    • acquireInterruptibly(int arg):与acquire(int arg)相同,但是该方法响应中断,当前线程为获取到同步状态而进入到同步队列中,如果当前线程被中断,则该方法会抛出InterruptedException异常并返回;
    • tryAcquire(int arg):独占式获取同步状态,获取同步状态成功后,其他线程需要等待该线程释放同步状态才能获取同步状态;
    • tryAcquireNanos(int arg,long nanos):超时获取同步状态,如果当前线程在nanos时间内没有获取到同步状态,那么将会返回false,已经获取则返回true;
    • release(int arg):独占式释放同步状态,该方法会在释放同步状态之后,将同步队列中第一个节点包含的线程唤醒;
    • tryRelease(int arg):独占式释放同步状态;
  2. 共享式:同一时刻可以有多个线程获得锁,比如读锁多个线程获得,写锁一个线程获得
    • acquireShared(int arg):共享式获取同步状态,如果当前线程未获取到同步状态,将会进入同步队列等待,与独占式的主要区别是在同一时刻可以有多个线程获取到同步状态;
    • acquireSharedInterruptibly(int arg):共享式获取同步状态,响应中断;
    • tryAcquireShared(int arg):共享式获取同步状态,返回值大于等于0则表示获取成功,否则获取失败;
    • tryAcquireSharedNanos(int arg, long nanosTimeout):共享式获取同步状态,增加超时限制;
    • releaseShared(int arg):共享式释放同步状态;
    • tryReleaseShared(int arg):共享式释放同步状态;
  3. 查询同步队列线程情况
    • isHeldExclusively():当前同步器是否在独占式模式下被线程占用,一般该方法表示是否被当前线程所独占;

我们接下来就挑选最基础的获取和释放锁来看看是怎么处理的。

独占式

获取锁

  1. public final void acquire(int arg) {
  2. if (!tryAcquire(arg) &&
  3. acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
  4. selfInterrupt();
  5. }
  6. //子类实现
  7. protected boolean tryAcquire(int arg) {
  8. throw new UnsupportedOperationException();
  9. }
  10. final boolean acquireQueued(final Node node, int arg) {
  11. //是否失败
  12. boolean failed = true;
  13. try {
  14. //是否中断
  15. boolean interrupted = false;
  16. //死循环自旋
  17. for (;;) {
  18. //获取前节点
  19. final Node p = node.predecessor();
  20. //若前节点为头节点且获取锁成功
  21. if (p == head && tryAcquire(arg)) {
  22. //设置头节点
  23. setHead(node);
  24. p.next = null; // help GC
  25. fail = false;
  26. return interrupted;
  27. }
  28. //获取失败,判断是否需要阻塞 后文介绍(线程阻塞)
  29. if (shouldParkAfterFailedAcquire(p, node) &&
  30. parkAndCheckInterrupt())
  31. interrupted = true;
  32. }
  33. } finally {
  34. if (failed)
  35. cancelAcquire(node);
  36. }
  37. }

tryAcquire 是交由子类同步器实现的方法,具体就是尝试获取锁,如果成功则直接返回;否则执行addWaiter(Node.EXCLUSIVE) 该线程加入到FIFO队列尾部;然后执行 acquireQueued 线程会进入一个死循环自旋的过程去获取锁直到退出,并返回是否被中断。

释放锁

  1. public final boolean release(int arg) {
  2. if (tryRelease(arg)) { //尝试释放锁
  3. Node h = head;
  4. if (h != null && h.waitStatus != 0)
  5. //唤醒后继节点 该方法后面会介绍
  6. unparkSuccessor(h);
  7. return true;
  8. }
  9. return false;
  10. }
  11. //子类实现
  12. protected boolean tryRelease(int arg) {
  13. throw new UnsupportedOperationException();
  14. }

tryReleasetryAcquire 一样是交由子类同步器实现的方法,具体则是尝试释放锁;如果成功则调用unparkSuccessor唤醒后继节点。

共享式

获取锁

  1. public final void acquireShared(int arg) {
  2. if (tryAcquireShared(arg) < 0)
  3. doAcquireShared(arg);
  4. }
  5. //由子类实现
  6. protected int tryAcquireShared(int arg) {
  7. throw new UnsupportedOperationException();
  8. }
  9. private void doAcquireShared(int arg) {
  10. //加入队列尾部
  11. final Node node = addWaiter(Node.SHARED);
  12. boolean failed = true;
  13. try {
  14. boolean interrupted = false;
  15. //死循环自旋
  16. for (;;) {
  17. //前继节点
  18. final Node p = node.predecessor();
  19. if (p == head) { //头结点
  20. //尝试获取锁
  21. int r = tryAcquireShared(arg);
  22. if (r >= 0) {
  23. setHeadAndPropagate(node, r);
  24. p.next = null; // help GC
  25. if (interrupted)
  26. selfInterrupt();
  27. failed = false;
  28. return;
  29. }
  30. }
  31. if (shouldParkAfterFailedAcquire(p, node) &&
  32. parkAndCheckInterrupt())
  33. interrupted = true;
  34. }
  35. } finally {
  36. if (failed)
  37. cancelAcquire(node);
  38. }
  39. }

tryAcquireShared 则不多作介绍都是由子类实现,这里要注意的是共享式返回值是 int ,只要该值大于等于0则表示获取到了锁,否则通过doAcquireShared加入队列自旋获取锁。 (整个过程类似于独占式获取锁)

释放锁

  1. public final boolean releaseShared(int arg) {
  2. if (tryReleaseShared(arg)) {
  3. doReleaseShared();
  4. return true;
  5. }
  6. return false;
  7. }
  8. //子类实现
  9. protected boolean tryReleaseShared(int arg) {
  10. throw new UnsupportedOperationException();
  11. }
  12. private void doReleaseShared() {
  13. //死循环 自旋
  14. for (;;) {
  15. //获取头节点
  16. Node h = head;
  17. if (h != null && h != tail) { //不是只有一个节点
  18. int ws = h.waitStatus; //获取节点状态
  19. if (ws == Node.SIGNAL) { //唤醒后继节点
  20. if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)) //cas释放
  21. continue; // loop to recheck cases
  22. unparkSuccessor(h);
  23. }
  24. else if (ws == 0 && //初始化状态
  25. !compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) //cas释放
  26. continue; // loop on failed CAS
  27. }
  28. if (h == head) // loop if head changed
  29. break;
  30. }
  31. }

可能存在多个线程同时进行释放同步资源(读锁),所以通过CAS操作需要确保同步状态安全成功释放。

线程阻塞

  1. private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
  2. //前驱节点等待状态
  3. int ws = pred.waitStatus;
  4. if (ws == Node.SIGNAL) //前驱节点处于处于等待状态 直接返回
  5. return true;
  6. if (ws > 0) { // 前驱节点被中断或取消,需要从队列中移除 直到所有中断或取消的前驱节点全部被移除
  7. do {
  8. node.prev = pred = pred.prev;
  9. } while (pred.waitStatus > 0);
  10. pred.next = node;
  11. } else { // ws < 0 即 -2 -3的情况 CONDITION PROPAGATE
  12. compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
  13. }
  14. return false;
  15. }

该段代码处于循环获取锁的过程中(acquireQueued),在自旋的过程中需要判断当前线程是否需要阻塞。该方法依靠前驱节点来判断是否需要阻塞。如果返回true则调用下列方法。

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

该方法调用主要使用LockSupport把线程挂起,阻塞住线程的调用栈,同时返回当前线程的中断状态。

线程唤醒

  1. private void unparkSuccessor(Node node) {
  2. //当前节点状态
  3. int ws = node.waitStatus;
  4. if (ws < 0) //节点状态为0 设置为0
  5. compareAndSetWaitStatus(node, ws, 0);
  6. //后继节点
  7. Node s = node.next;
  8. if (s == null || s.waitStatus > 0) { //后继节点为空或被中断取消
  9. s = null;
  10. //从尾节点向前找到可用节点
  11. for (Node t = tail; t != null && t != node; t = t.prev)
  12. if (t.waitStatus <= 0)
  13. s = t;
  14. }
  15. //唤醒后继节点
  16. if (s != null)
  17. LockSupport.unpark(s.thread);
  18. }

当线程被阻塞且前一个线程释放锁后,调用unparkSuccessor方法来唤醒后继节点。从尾节点开始寻找一个最近可用的节点,为什么从最后一个找是因为正着找可能后继节点仍然会是为null或取消。此处尾节点开始找到之后并没有退出循环,而是继续往前找,直到找到距离释放锁最近的一个节点。

参考链接

【死磕Java并发】—–J.U.C之AQS:AQS简介
【死磕Java并发】—–J.U.C之AQS:CLH同步队列
【死磕Java并发】—–J.U.C之AQS:同步状态的获取与释放
【死磕Java并发】—–J.U.C之AQS:阻塞和唤醒线程