Java高并发之AQS的实现分析(同步队列、独占式与共享式获取与释放同步状态)

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接: https://blog.csdn.net/u014454538/article/details/97689998
  • 从同步器的同步队列独占式获取和释放同步状态共享式获取和释放同步状态等核心数据结构和模板方法,分析同步器的实现。

1. 同步队列

① 同步队列的基本结构
  • 同步器依赖内部的同步队列(一种FIFO双向队列)实现同步状态的管理:
  1. 当前线程获取同步状态失败,同步器将当前线程和等待状态等信息封装成Node节点,添加到同步队列的尾部。同时,会阻塞当前线程。
  2. 同步状态释放,会唤醒同步队列的首节点中的线程,使其再次尝试获取同步状态。
  • Node节点中的重要属性:prev和next,构成双向队列。
  1. Node prev: 用于指向当前节点的前驱节点
  2. Node next: 用于指向当前节点的后继节点
  • 同步队列的重要属性:head和tail
  1. Node head: 指向头结点的引用,即指向队列的头结点(但head并非真正的头结点)
  2. Node tail: 指向尾节点的引用,即指向队列的尾节点(但tail并非真正的尾节点)
  • 同步队列的基本结构,如下图所示。
    在这里插入图片描述
② 节点的添加——设置尾节点
  • 由于同步队列是一个FIFO的双向队列,每次新添加的节点都从队列尾部进行添加。
  • 当一个线程成功获取了同步状态,其他线程只能被同步器封装成节点添加到队列中。
  • 节点入队的过程,必须保证线程安全。原因: 在多线程环境下,可能造成添加到同步队列的节点顺序错误或者数量不对
  • 为了保证线程安全,同步器提供了一种CAS设置尾节点的方法:compareAndSetTail(Node expect, Node update)。通过expect的限定,保证节点的加入是有序的。
    在这里插入图片描述
  1. 当expect节点为队列的原尾节点时,将新尾节点的prev引用指向原尾节点,将原尾节点的next引用指向新尾节点。
  2. 将队列的tail引用指向新尾节点,完成CAS设置尾节点。
③ 设置头结点
  • 头结点成功获取同步状态的节点,头结点中的线程在释放同步状态的同时,也会唤醒后继t节点中的线程。 如果后继节点中的线程成功获取同步状态,将会成为新的头结点。
  • 设置头结点是由获取同步状态成功的线程来完成,由于只有一个线程能获取到同步状态,因此头结点的设置不需要CAS保证
    在这里插入图片描述
  1. 将head引用指向新头结点
  2. 断开原头结点和新头节点之间的prev和next引用。

2. 独占式获取和释放同步状态

① 独占式获取同步状态
  • 独占式获取同步状态,使用同步器中的acquire()模板方法。节点中的线程无法响应中断,发生中断后,不会从同步队列中移出。
public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}
  • acquire()模板方法主要完成了同步状态获取节点构造加入同步队列以及在同步队列中自旋等待的相关工作,其主要逻辑是:
  1. 当前线程通过调用tryAcquire()可重写方法独占式的获取同步状态,获取成功直接返回。
  2. 获取同步状态失败,构建独占式同步节点Node.EXCLUSIVE),并调用addWaiter(Node mode)方法将该节点加入到同步队列中。
  3. 最后调用acquireQueued(finalNode node, int args)方法,使该节点以死循环的方式获取同步状态。如果获取同步状态失败则阻塞节点中的线程,直到该节点的前驱节点出队或者阻塞线程被中断,才能唤醒阻塞线程。
  • acquire()方法的调用流程:
    在这里插入图片描述
② 同步节点的自旋
  • 调用acquireQueued(finalNode node, int args)方法,会让节点以死循环的方式获取同步状态,这种死循环叫做节点的自旋
  • 节点进入同步队列后,就进入了一个自旋的过程:每个节点(或者说每个线程)都在自省的观察,当条件满足,获取到了同步状态,就可以从这个自旋过程中退出。否则, 依旧留在这个自旋过程中。
final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            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);
    }
}
  • 自己对自旋的理解:
    在这里插入图片描述
  1. 当前线程获取同步状态失败,会被封装成同步节点并加入到同步队列中。加入到同步队列中的线程是阻塞的,等待被唤醒。
  2. 节点中的线程被唤醒,如果该节点的前驱节点是头节点获取同步状态成功,则该节点会退出旋过程,然后将自己设置为新的头结点
  3. 节点中的线程被唤醒,如果发现自己的前驱节点不是头结点或者获取同步状态失败,则会重新阻塞,等待下一次被唤醒。
  • acquireQueued()方法中,要求只有前驱节点是头结点才能获取同步状态,这样做的原因:
  1. 头结点是获取同步状态的节点,头结点释放同步状态的同时将会唤醒后继节点,后继节点中的线程将会检查自己的前驱节点是否为头结点。
  2. 这样可以保证同步队列的FIFO规则。
    在这里插入图片描述
  • 处于自旋的同步节点各自独立地检查自己的状态,通过简单的判断自己前驱节点是否为头结点,既可以保证同步队列的FIFO原则,又能方便地处理过早被通知的情况。
  • 过早被通知: 前驱节点不是头结点的线程因为中断而被唤醒。
③ 独占式的释放同步状态
  • 调用同步器的release()模板方法,实现独占式的释放同步状态。
public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}
  1. 如果头结点成功释放同步状态,调用unparkSuccessor()方法唤醒后继节点线程。
  2. unparkSuccessor()内部通过LockSuppor.unpark()方法,唤醒于阻塞的线程。

3. 共享式的获取和释放同步状态

① 共享式的获取同步状态
  • 共享式获取与独占式获取的区别: 同一时刻能否有多个线程同时获取到同步状态
  • 以文件的读写为例:
  1. 如果有一个程序在读文件,那么这一时刻的写操作均被阻塞,而读操作能够同时进行
  2. 如果有一个程序在写文件,那么这一时刻不管是其他的写操作还是读操作,均被阻塞。
  3. 写操作要求对资源的独占式访问,而读操作可以是共享式访问
    在这里插入图片描述
  • 调用同步器的acquireShared()模板方法,可以实现共享式获取同步状态。
public final void acquireShared(int arg) {
    if (tryAcquireShared(arg) < 0)
        doAcquireShared(arg);
}
  1. 当前线程首先调用tryAcquireShared()可重写方法,共享式的获取同步状态。如果返回值大于等于0,表示获取成功并返回。
  2. 如果返回值小于表示获取失败,调用doAcquireShared()方法,让线程进入自旋状态。
  3. 自旋过程中,如果当前节点的前驱节点是头结点,且调用tryAcquireShared()方法返回值大于等于0,则退出自旋。否则,继续进行自旋。
② 共享式的释放同步状态
  • 调用releaseShared()模板方法,共享式的释放同步状态。
public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) {
        doReleaseShared();
        return true;
    }
    return false;
}
  • 共享式释放同步状态,使得同步状态的释放可能同时来自多个线程。为了确保同步状态的安全释放,一般通过循环和CAS来保证。

4. 关于AQS的问题总结

1. 什么是AQS?与锁的区别和联系?

  • AQS:AbstractQueuedSynchronizer,队列同步器。
  • AQS与锁的区别和联系:
  1. AQS是实现锁和其他同步组件的基础框架
  2. AQS是面向锁的实现者,它简化了锁的实现方式,屏蔽了同步状态管理线程的排队、等待与唤醒等底层操作。
  3. 锁是面向使用者,它定义了使用者与锁交互的接口,隐藏了实现细节。
  4. AQS和锁有效的隔离了实现者和使用者所需关注的领域。

2. AQS中,自定义同步器需要重写哪些方法?

  • 基础: AQS中5种指定的可重写方法的名称、作用,tryAcquire()的实现:compareAndSetState()
  • 进阶: 以实现锁为例,讲解如何使用同步器的实现自定义锁。

3. AQS是什么?底层如何实现?

  • 关于如何为实现:
  1. 两个核心(同步状态和同步队列)
  2. 更改同步状态的三种核心方法
  3. 同步队列的结构、如何设置尾节点(需要CAS保证)和头结点(不需要CAS保证)。

猜你喜欢

转载自blog.csdn.net/u014454538/article/details/97689998