【JUC】AQS(AbstractQueuedSynchronizer, 抽象队列同步器)

16 篇文章 1 订阅

重要性

AQS相对于JUC的重要性,就好比JVM相对于Java的重要性

前置知识

  • 公平锁和非公平锁
    • 公平锁:锁被释放以后,先申请的线程先得到锁。性能较差一些,因为公平锁为了保证时间上的绝对顺序,上下文切换更频繁
    • 非公平锁:锁被释放以后,后申请的线程可能会先获取到锁,是随机或者按照其他优先级排序的。性能更好,但可能会导致某些线程永远无法获取到锁
  • 可重入锁
    • 也叫做递归锁,指的是线程可以再次获取自己的内部锁,比如一个线程获取到了对象锁,此时这个对象锁还没有释放,当其想再次获取这个对象锁的时候还是可以获取的,如果不可重入的话,会导致死锁。
  • 自旋思想
    • 当线程请求锁时,如果锁已经被其他线程持有,那么该线程会不断地重试获取锁,而不是被挂起等待,这种不断尝试获取锁的行为称为自旋
  • LockSupport
    • 一个工具类,用于线程的阻塞和唤醒操作,类似于wait()和notify()方法,但是更加灵活和可控
    • 提供了park()和unpark()两个静态方法用于线程阻塞和唤醒操作。
    • 优点在于可以在任意时刻阻塞和唤醒线程而不需要事先获取锁或监视器对象。
  • 数据结构之双向链表
    • 双向链表(Doubly Linked List)是一种常见的数据结构,它是由一系列节点(Node)组成的,每个节点包含三个部分:数据域、前驱指针和后继指针。其中,数据域存储节点的数据,前驱指针指向前一个节点,后继指针指向后一个节点。通过这种方式,双向链表可以实现双向遍历和插入、删除操作。
  • 设计模式之模板设计模式
    • 模板设计模式是一种行为型设计模式,定义了一种算法的框架,并将某些步骤延迟到子类中事先,这种设计模式的主要目的是允许子类在不改变算法结构的情况下重新定义算法中的某些步骤。
    • 优点是能够提高代码复用性和可维护性。

AQS入门级别理论知识

是什么?

AQS的中文翻译是抽象的队列同步器,源码位置如下,我们所说的AQS指的是蓝色框中的

在这里插入图片描述

AbstractOwnableSynchronizerAbstractQueuedLongSynchronizerAQS的父类,AQS自JDK1.5产生,AbstractQueuedLongSynchronizer自JDK1.6产生

技术解释

  • 是用来实现锁或者其他同步器组件的公共基础部分的抽象实现
  • 是重量级基础框架及整个JUC体系的基石,主要用于解决锁分配给“谁”的问题
  • 整体就是一个抽象的FIFO队列来完成资源获取线程的排队工作,并通过一个int类变量表示持有锁的状态

官网解释

在这里插入图片描述

整体就是一个抽象的FIFO队列来完成资源获取线程的排队工作,并通过一个int类变量表示持有锁的状态,AQS可以抽象为如下图片的模型

在这里插入图片描述

AQS为什么是JUC的基石

和AQS有关的组件如下,这些类都是由AQS在底部默默支撑

在这里插入图片描述

  • ReentrantLock
    在这里插入图片描述

  • CountDownLatch
    在这里插入图片描述

  • ReentrantReadWriteLock
    在这里插入图片描述

  • Semaphore
    在这里插入图片描述

  • 进一步理解锁和同步器的关系

    • 锁,面向锁的使用者:定义了程序员和锁交互的使用层API,隐藏了实现细节,你调用即可
    • 同步器,面向锁的实现者:Java并发大神DoungLee,提出了统一规范并简化了锁的实现,将其抽象出来,屏蔽了同步状态管理、同步队列的管理和维护、阻塞线程排队和通知、唤醒机制等,是一切锁和同步组件实现的公共基础部分

能干嘛?

加锁会导致阻塞,有阻塞就需要排队,实现排队必然需要队列

  • 抢到资源的线程直接使用处理业务,抢不到资源的必然涉及一种排队等候机制。抢占失败的线程继续去等待(类似于银行办理窗口都满了,暂时没有受理窗口的顾客只能去候客区排队等待),但等候线程仍然保留获取锁的可能且获取锁流程仍在继续(候客区的顾客也在等着叫号,轮到了再去受理窗口办理业务)
  • 既然说到了排队等候机制,那么就一定会有某种队列形成,这样的队列是什么数据结构呢?
    • 如果共享资源被占用,就需要一定的阻塞等待唤醒机制来保证锁分配。这个机制主要用的是CLH队列的变体实现的,将暂时获取不到锁的线程加入到队列中,这个队列就是AQS同步队列的抽象表现。它将要请求共享资源的线程及自身的等待状态封装成队列的节点对象(Node),通过CAS、自旋以及LockSupport.park()的方式,维护着state变量的状态,使其达到同步的状态。
    • AQS使用一个volatile的int类型的成员变量来表示同步状态,通过内置的FIFO队列来完成资源获取的排队工作将每条要去抢占资源的线程封装成一个Node节点来实现锁的分配,通过CAS完成对State值的修改。

在这里插入图片描述

小总结

AQS同步队列的基本结构

在这里插入图片描述

CLH:Craig、Landin and Hagersten队列,是个单向链表,AQS中的队列是CLH变体的虚拟双同队列(FIFO)

AQS源码分析前置知识

AQS内部体系架构图

在这里插入图片描述

AQS内部体系架构:State变量+CLH双端队列

  • AQS的int类型变量state
    在这里插入图片描述

    • 类比于银行办理业务的受理窗口状态
      • =0,没人,自由状态可以去办理
      • ≥1,有人占用窗口,排队等着
  • AQS的CLH队列

    • CLH(三个大牛的名字组成)队列为一个双向队列
      在这里插入图片描述

    • 类比于银行候客区的等待顾客

  • 小总结

    • 有阻塞就需要排队,实现排队必然需要队列,队列里面需要一些等待唤醒机制

AQS内部体系架构:内部类Node

简单来说

  • 队列中每个排队的个体就是一个Node(队列中的元素)

  • Node内部的waitStatus属性表示等候区其他顾客(其他线程)的等待状态

  • Node此类的讲解

    • 内部结构

      • static final class Node {
            /** Marker to indicate a node is waiting in shared mode */
            // Node是共享型
            static final Node SHARED = new Node();
            /** Marker to indicate a node is waiting in exclusive mode */
            // Node是独占型
            static final Node EXCLUSIVE = null;
        
            /** waitStatus value to indicate thread has cancelled */
            // 队列中的节点不想排队了,被取消了
            static final int CANCELLED =  1;
            /** waitStatus value to indicate successor's thread needs unparking */
            // 后续线程需要被唤醒
            static final int SIGNAL    = -1;
            /** waitStatus value to indicate thread is waiting on condition */
            // 在condition队列等待的情况,等待condition唤醒
            static final int CONDITION = -2;
            /**
             * waitStatus value to indicate the next acquireShared should
             * unconditionally propagate
             */
             // 传播,共享式同步状态获取将会无条件传播下去
            static final int PROPAGATE = -3;
        
            /**
             * Status field, taking on only the values:
             *   SIGNAL:     The successor of this node is (or will soon be)
             *               blocked (via park), so the current node must
             *               unpark its successor when it releases or
             *               cancels. To avoid races, acquire methods must
             *               first indicate they need a signal,
             *               then retry the atomic acquire, and then,
             *               on failure, block.
             *   CANCELLED:  This node is cancelled due to timeout or interrupt.
             *               Nodes never leave this state. In particular,
             *               a thread with cancelled node never again blocks.
             *   CONDITION:  This node is currently on a condition queue.
             *               It will not be used as a sync queue node
             *               until transferred, at which time the status
             *               will be set to 0. (Use of this value here has
             *               nothing to do with the other uses of the
             *               field, but simplifies mechanics.)
             *   PROPAGATE:  A releaseShared should be propagated to other
             *               nodes. This is set (for head node only) in
             *               doReleaseShared to ensure propagation
             *               continues, even if other operations have
             *               since intervened.
             *   0:          None of the above
             *
             * The values are arranged numerically to simplify use.
             * Non-negative values mean that a node doesn't need to
             * signal. So, most code doesn't need to check for particular
             * values, just for sign.
             *
             * The field is initialized to 0 for normal sync nodes, and
             * CONDITION for condition nodes.  It is modified using CAS
             * (or when possible, unconditional volatile writes).
             */
            // 表示node的等待状态,取值是上面的CANCELLED、SIGNAL……,初始值为0
            volatile int waitStatus;
        
            /**
             * Link to predecessor node that current node/thread relies on
             * for checking waitStatus. Assigned during enqueuing, and nulled
             * out (for sake of GC) only upon dequeuing.  Also, upon
             * cancellation of a predecessor, we short-circuit while
             * finding a non-cancelled one, which will always exist
             * because the head node is never cancelled: A node becomes
             * head only as a result of successful acquire. A
             * cancelled thread never succeeds in acquiring, and a thread only
             * cancels itself, not any other node.
             */
            volatile Node prev;
        
            /**
             * Link to the successor node that the current node/thread
             * unparks upon release. Assigned during enqueuing, adjusted
             * when bypassing cancelled predecessors, and nulled out (for
             * sake of GC) when dequeued.  The enq operation does not
             * assign next field of a predecessor until after attachment,
             * so seeing a null next field does not necessarily mean that
             * node is at end of queue. However, if a next field appears
             * to be null, we can scan prev's from the tail to
             * double-check.  The next field of cancelled nodes is set to
             * point to the node itself instead of null, to make life
             * easier for isOnSyncQueue.
             */
            volatile Node next;
        
            /**
             * The thread that enqueued this node.  Initialized on
             * construction and nulled out after use.
             */
            // 节点所封装的线程
            volatile Thread thread;
        
            /**
             * Link to next node waiting on condition, or the special
             * value SHARED.  Because condition queues are accessed only
             * when holding in exclusive mode, we just need a simple
             * linked queue to hold nodes while they are waiting on
             * conditions. They are then transferred to the queue to
             * re-acquire. And because conditions can only be exclusive,
             * we save a field by using special value to indicate shared
             * mode.
             */
            Node nextWaiter;
        
            /**
             * Returns true if node is waiting in shared mode.
             */
            final boolean isShared() {
                return nextWaiter == SHARED;
            }
        
            /**
             * Returns previous node, or throws NullPointerException if null.
             * Use when predecessor cannot be null.  The null check could
             * be elided, but is present to help the VM.
             *
             * @return the predecessor of this node
             */
            // 返回前置节点
            final Node predecessor() throws NullPointerException {
                Node p = prev;
                if (p == null)
                    throw new NullPointerException();
                else
                    return p;
            }
        
            Node() {    // Used to establish initial head or SHARED marker
            }
        
            Node(Thread thread, Node mode) {     // Used by addWaiter
                this.nextWaiter = mode;
                this.thread = thread;
            }
        
            Node(Thread thread, int waitStatus) { // Used by Condition
                this.waitStatus = waitStatus;
                this.thread = thread;
            }
        }
        
    • 属性说明

在这里插入图片描述

AQS源码深度讲解和分析

ReentrantLock的原理

Lock接口的实现类,基本都是通过聚合了一个队列同步器的子类完成线程访问控制的

在这里插入图片描述

ReentrantLock公平、非公平锁源码解读

创建ReentrantLock对象的时候,会根据参数判断是使用FairSync还是NonFairSync

在这里插入图片描述

ReentrantLock其实就是操作AQS的子类Sync,Sync又有子类FairSyncNonfairSync来实现公平锁和非公平锁

在这里插入图片描述

两个静态类lock方法的实现有所不同:非公平锁第一次执行lock可以直接执行compareAndSetState来抢锁,因为不需要管队列顺序

在这里插入图片描述

在这里插入图片描述

  • 第一个线程:CAS,如果等于期望值0,说明我是第一个线程,将state从0改为1,并将持有资源占有锁的线程修改为当前线程
  • 第二个线程:发现已经有人占有锁了,只能调用acquire方法后续去抢占锁

公平锁不行,需要在tryAcquire中考虑当前线程是否为第一线程

在这里插入图片描述

acquire方法会调用tryAcquire方法来抢锁,抢锁不成功的话,会调用addWaiter来将Node加入到等待队列

public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

acquire方法主要做了这些事情,在下面的银行办理业务案例中详细演示流程

在这里插入图片描述

tryAcquire方法使用模板设计模式,父类的方法只是抛出一个异常,不加以实现,交给子类FairSyncNonfairSync

在这里插入图片描述

在这里插入图片描述

公平锁在抢锁之前,会判断队列中是否有节点处于等待

在这里插入图片描述

判断线程是否有任何前驱者在同步队列中等待。如果存在这样的前驱者,那么当前线程可能需要等待才能获取到锁或其他同步状态

在这里插入图片描述

非公平锁不需要执行该方法,直接抢锁就行

在这里插入图片描述

公平锁和非公平锁的tryAcquire方法唯一的区别就在于公平锁在获取同步状态时多了一个限制条件:hasQueuedPredecessors():公平锁加锁时判断等待队列中是否存在有效节点的方法

银行办理业务案例讲解

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class AQSDemo {
    public static void main(String[] args) {
        ReentrantLock reentrantLock = new ReentrantLock();//非公平锁
        // A B C三个顾客,去银行办理业务,A先到,此时窗口空无一人,他优先获得办理窗口的机会,办理业务。
        // A 耗时严重,估计长期占有窗口
        new Thread(() -> {
            reentrantLock.lock();
            try {
                System.out.println("----come in A");
                //暂停20分钟线程
                try {
                    TimeUnit.MINUTES.sleep(20);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            } finally {
                reentrantLock.unlock();
            }
        }, "A").start();

        // B是第2个顾客,B一看到受理窗口被A占用,只能去候客区等待,进入AQS队列,等待着A办理完成,尝试去抢占受理窗口。
        new Thread(() -> {
            reentrantLock.lock();
            try {
                System.out.println("----come in B");
            } finally {
                reentrantLock.unlock();
            }
        }, "B").start();


        // C是第3个顾客,C一看到受理窗口被A占用,只能去候客区等待,进入AQS队列,等待着A办理完成,尝试去抢占受理窗口,前面是B顾客,FIFO
        new Thread(() -> {
            reentrantLock.lock();
            try {
                System.out.println("----come in C");
            } finally {
                reentrantLock.unlock();
            }
        }, "C").start();

        // 后续顾客DEFG。。。。。。。以此类推
        new Thread(() -> {
            reentrantLock.lock();
            try {
                //。。。。。。
            } finally {
                reentrantLock.unlock();
            }
        }, "D").start();
    }
}
lock:客户B、C入队

在这里插入图片描述

1、第一个顾客A过来

在这里插入图片描述

2、第二个顾客B过来,因为顾客A占用锁,B调用tryAcquire只能返回false(因为state!=0且占用锁的线程不是B线程)

在这里插入图片描述

tryAcquire(arg)返回false,!tryAcquire(arg)为true,继续执行下面的代码来加入队列。Node.EXCLUSIVE:独占的Node节点

在这里插入图片描述

一开始队列为空,执行enq入队

在这里插入图片描述

双向链表中,第一个节点为虚节点(也叫哨兵节点),其实并不存储任何信息,只是占位。真正的第一个有数据的节点,是从第二个节点开始的。

在这里插入图片描述

第一轮循环,虚拟空节点入队

在这里插入图片描述

进入第二轮循环,将B节点入队,return跳出循环

在这里插入图片描述

到这里B节点入队完成

在这里插入图片描述

3、第三个顾客C过来,tryAcquire也是返回false,C也入队。其他顾客过来也是一样的道理

在这里插入图片描述

在这里插入图片描述

客户线程挂起

顾客B尝试抢占资源,抢不到,调用shouldParkAfterFailedAcquire

在这里插入图片描述

第一次循环,shouldParkAfterFailedAcquire将前置节点的状态从0设置为-1,说明后面的线程需要被唤醒

在这里插入图片描述

在这里插入图片描述

第二次循环,shouldParkAfterFailedAcquire将线程B挂起,后续的其他线程也同理一一挂起

在这里插入图片描述

unLock:A释放资源

在这里插入图片描述

在这里插入图片描述

设置状态为0,并将占用锁的线程清空

在这里插入图片描述

锁释放成功,让其他线程来抢占锁

在这里插入图片描述

在这里插入图片描述

B被唤醒之后,继续进行循环调用tryAcquire来抢锁

在这里插入图片描述

在这里插入图片描述

将头节点设置为B

在这里插入图片描述

在这里插入图片描述

异常情况cancelAcquire

当出现异常的时候,会调用cancelAcquire将Node踢出队列

在这里插入图片描述

在这里插入图片描述

private void cancelAcquire(Node node) {
    // Ignore if node doesn't exist
    if (node == null)
        return;

    node.thread = null;

    // Skip cancelled predecessors
    // 假设4号节点出队的同时,三号节点也要出队,就会执行这里
    Node pred = node.prev;
    while (pred.waitStatus > 0)
        node.prev = pred = pred.prev;

    // predNext is the apparent node to unsplice. CASes below will
    // fail if not, in which case, we lost race vs another cancel
    // or signal, so no further action is necessary.
    Node predNext = pred.next;

    // Can use unconditional write instead of CAS here.
    // After this atomic step, other Nodes can skip past us.
    // Before, we are free of interference from other threads.
    node.waitStatus = Node.CANCELLED;

    // If we are the tail, remove ourselves.
    // compareAndSetTail(node, pred) 将队尾节点设置为前置节点
    if (node == tail && compareAndSetTail(node, pred)) {
        // --if-- 队尾节点出队
        // 前置节点的next设置为null
        compareAndSetNext(pred, predNext, null);
    } else {
        // If successor needs signal, try to set pred's next-link
        // so it will get one. Otherwise wake it up to propagate.
        int ws;
        if (pred != head &&
            ((ws = pred.waitStatus) == Node.SIGNAL ||
             (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
            pred.thread != null) {
            Node next = node.next;
            if (next != null && next.waitStatus <= 0)
                compareAndSetNext(pred, predNext, next);
        } else {
            unparkSuccessor(node);
        }

        node.next = node; // help GC
    }
}

AQS源码总结

AQS的源码整理图可以参考大佬网友的复刻版本:AQS源码整理图

图中缺少如下内容:

在这里插入图片描述

美团AQS解析

https://tech.meituan.com/2019/12/05/aqs-theory-and-apply.html

文章说明

该文章是本人学习 尚硅谷 的学习笔记,文章中大部分内容来源于 尚硅谷 的视频尚硅谷JUC并发编程(对标阿里P6-P7),也有部分内容来自于自己的思考,发布文章是想帮助其他学习的人更方便地整理自己的笔记或者直接通过文章学习相关知识,如有侵权请联系删除,最后对 尚硅谷 的优质课程表示感谢。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Hello Dam

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值