别走!这里有个笔记:图文讲解 AQS ,一起看看 AQS 的源码……(图文较长)(二)

简介: AbstractQueuedSynchronizer 抽象队列同步器,简称 AQS 。是在 JUC 包下面一个非常重要的基础组件,JUC 包下面的并发锁 ReentrantLock CountDownLatch 等都是基于 AQS 实现的。所以想进一步研究锁的底层原理,非常有必要先了解 AQS 的原理。

共享模式

获取共享资源 acquireShared

public final void acquireShared(int arg) {
    // 小于 0 表示获取资源失败
    if (tryAcquireShared(arg) < 0)
        doAcquireShared(arg);
}
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) {
                    // 设置当前节点为头节点 并尝试唤醒后续节点
                    setHeadAndPropagate(node, r);
                    // 释放头节点 GC 会回收
                    p.next = null; // help GC
                    if (interrupted)
                        selfInterrupt();
                    failed = false;
                    return;
                }
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}
  1. tryAcquireShared(arg),尝试获取资源,这块由子类实现;
  2. 返回值分为 3 种:
  1. 小于 0: 表示失败;
  2. 等于 0: 表示共享模式获取资源成功,但后续的节点不能以共享模式获取成功;
  3. 大于 0: 表示共享模式获取资源成功,后续节点在共享模式获取也可能会成功,在这种情况下,后续等待线程必须检查可用性。
  1. 在失败后会使用 doAcquireShared(arg); 不断获取资源;
  2. final Node node = addWaiter(Node.SHARED); 同样会创建节点;
  3. 在循环中不断判断前一个节点如果是 head,则尝试获取资源;
  4. 在共享模式下获取到资源后会使用 setHeadAndPropagate(node, r); 设置头节点,同时唤醒后续节点。
设置头节点,并传播唤醒后续节点
// node 是当前节点
// propagate 是 前一步 tryAcquireShared 的返回值 进来时 >=0
// 大于 0: 表示共享模式获取资源成功,后续节点在共享模式获取也可能会成功,在这种情况下,后续等待线程必须检查可用性。
private void setHeadAndPropagate(Node node, int propagate) {
    // 记录下当前头节点
    Node h = head; // Record old head for check below
    // 设置传入 node 为头节点
    setHead(node);
    // 判断条件,唤醒后续节点
    // propagate > 0 有后续资源
    // h == null 旧的头节点 因为前面 addWaiter, 肯定不会为空,应该是防止 h.waitStatus < 0 空指针的写法
    // (h = head) == null 当前的 头节点,再判断状态
    // waitStatus < 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();
    }
}
doReleaseShared() 释放共享资源
private void doReleaseShared() {
    // 循环
    for (;;) {
        // 从头开始
        Node h = head;
        // 判断队列是否为空,就是刚初始化
        if (h != null && h != tail) {
            int ws = h.waitStatus;
            // SIGNAL( -1 后续线程需要释放)
            if (ws == Node.SIGNAL) {
                // 将等待状态更新为 0 如果失败,会循环
                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                    continue;            // loop to recheck cases
                // 唤醒后续节点, 同时将当前节点设置为 取消
                unparkSuccessor(h);
            }
            // 如果状态是 0 则会更新状态为 PROPAGATE
            // PROPAGATE ( -3 releaseShared 应该被传播到其他节点)
            else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                continue;                // loop on failed CAS
        }
        // 判断头节点有没有变化,有变化 是因为竞争,别的线程获取到了锁,会继续循环
        // 没有变化直接结束
        if (h == head)                   // loop if head changed
            break;
    }
}
  1. 从头节点开始进行,如果 h != null && h != tail 说明队列不是空或者刚初始化;
  2. 节点状态为 SIGNAL( -1 )说明后续线程需要释放;
  3. 会更改当前节点状态,成功后唤醒后续节点,失败则继续循环;
  4. 节点状态如果是 0 则更新为 PROPAGATE,会将状态传播。

释放共享资源 releaseShared

public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) {
        // 释放共享资源
        doReleaseShared();
        return true;
    }
    return false;
}

以共享模式释放。 通过释放一个或多个线程,如果实现tryReleaseShared返回true。


总结


Q: AQS 到底是什么?


A: AQS 内部提供了一个先入先出(FIFO)双向等待队列,内部依靠 Node 实现,并提供了在独占模式共享模式下的出入队列的公共方法。而关于状态信息 state 的定义是由子类实现。tryAcquire、tryRelease、tryAcquireShared、tryReleaseShared等尝试获取资源操作都是由子类进行定义和实现的。而 AQS 中提供了子类获取资源之后的相关操作,包括节点 Node 的出入队列,自旋获取资源等等。


Q: AQS 获取资源失败后会如何操作?


A: 线程获取资源失败后,会放到等待队列中,在队列中会不断尝试获取资源(自旋),说明线程只是进入等待状态,后面还是可以再次获取资源的。


Q: AQS 等待队列的数据结构是什么?


A: CLH变体的先入先出(FIFO)双向等待队列。(CLH锁是一个自旋锁。能确保无饥饿性。提供先来先服务的公平性。是一种基于链表的可扩展、高性能、公平的自旋锁,申请线程仅仅在本地变量上自旋,它不断轮询前驱的状态,如果发现前驱释放了锁就结束自旋。)


Q: AQS 等待队列中的节点如何获取获取和释放资源的?


A: 可以看下独占模式中的讲述过程,通过代码梳理。


本文分别从 独占模式共享模式介绍的 AQS 基本逻辑,并通过源码和作图理解基本思路。但是并没有对需要子类实现的业务逻辑做介绍。这块会在后面介绍 ReentrantLockCountDownLatch 等子类的时候做介绍。

目录
相关文章
|
3天前
|
Java 网络虚拟化
从源码全面解析LinkedBlockingQueue的来龙去脉
从源码全面解析LinkedBlockingQueue的来龙去脉
|
3天前
|
数据采集 安全 Java
Java并发编程学习12-任务取消(上)
【5月更文挑战第6天】本篇介绍了取消策略、线程中断、中断策略 和 响应中断的内容
30 4
Java并发编程学习12-任务取消(上)
|
3天前
|
Java 编译器
从源码全面解析 ArrayBlockingQueue 的来龙去脉
从源码全面解析 ArrayBlockingQueue 的来龙去脉
|
设计模式 算法
深入了解ReentrantLock源码附带图文分析
ReentrantLock源码图文分析
82 0
深入了解ReentrantLock源码附带图文分析
|
机器学习/深度学习 安全 Java
java并发原理实战(10)--AQS 和公平锁分析
java并发原理实战(10)--AQS 和公平锁分析
100 0
java并发原理实战(10)--AQS 和公平锁分析
别走!这里有个笔记:图文讲解 AQS ,一起看看 AQS 的源码……(图文较长)(一)
AbstractQueuedSynchronizer 抽象队列同步器,简称 AQS 。是在 JUC 包下面一个非常重要的基础组件,JUC 包下面的并发锁 ReentrantLock CountDownLatch 等都是基于 AQS 实现的。所以想进一步研究锁的底层原理,非常有必要先了解 AQS 的原理。
94 0
|
设计模式 IDE 算法
万字超强图文讲解AQS以及ReentrantLock应用(建议收藏)(上)
万字超强图文讲解AQS以及ReentrantLock应用(建议收藏)
万字超强图文讲解AQS以及ReentrantLock应用(建议收藏)(上)
万字超强图文讲解AQS以及ReentrantLock应用(建议收藏)(下)
万字超强图文讲解AQS以及ReentrantLock应用(建议收藏)(下)
万字超强图文讲解AQS以及ReentrantLock应用(建议收藏)(下)
|
算法 Java API
硬核干货:5W字17张高清图理解同步器框架AbstractQueuedSynchronizer(下)
Doug Lea大神编写AQS是有严谨的理论基础的,他的个人博客上有一篇论文《The java.util.concurrent Synchronizer Framewor》,可以在互联网找到相应的译文《JUC同步器框架》,如果想要深入研究AQS必须要理解一下该论文的内容,然后结合论文内容详细分析一下AQS的源码实现。本文在阅读AQS源码的时候选用的JDK版本是JDK11。
138 0
硬核干货:5W字17张高清图理解同步器框架AbstractQueuedSynchronizer(下)
|
存储 算法 Java
硬核干货:5W字17张高清图理解同步器框架AbstractQueuedSynchronizer(上)
Doug Lea大神编写AQS是有严谨的理论基础的,他的个人博客上有一篇论文《The java.util.concurrent Synchronizer Framewor》,可以在互联网找到相应的译文《JUC同步器框架》,如果想要深入研究AQS必须要理解一下该论文的内容,然后结合论文内容详细分析一下AQS的源码实现。本文在阅读AQS源码的时候选用的JDK版本是JDK11。
151 1
硬核干货:5W字17张高清图理解同步器框架AbstractQueuedSynchronizer(上)