13.AbstractQueuedSynchronizer之AQS

本笔记来源于:尚硅谷JUC并发编程(对标阿里P6-P7)
b站视频

文章来自:
https://www.yuque.com/gongxi-wssld/csm31d/ln0mq1w7wp1oy99g
https://www.yuque.com/liuyanntes/vx9leh/fpy93i
https://blog.csdn.net/dolpin_ink/category_11847910.html

脑图

本地:
尚硅谷JUC并发编程

在线:
尚硅谷JUC并发编程

在线脑图加载时间超长。

1、前置知识

公平锁和非公平锁

  • 公平锁:锁被释放以后,先申请的线程先得到锁。性能较差一些,因为公平锁为了保证时间上的绝对顺序,上下文切换更频繁
  • 非公平锁:锁被释放以后,后申请的线程可能会先获取到锁,是随机或者按照其他优先级排序的。性能更好,但可能会导致某些线程永远无法获取到锁

可重入锁

  • 也叫做递归锁,指的是线程可以再次获取自己的内部锁,比如一个线程获取到了对象锁,此时这个对象锁还没有释放,当其想再次获取这个对象锁的时候还是可以获取的,如果不可重入的话,会导致死锁。

自旋思想

  • 当线程请求锁时,如果锁已经被其他线程持有,那么该线程会不断地重试获取锁,而不是被挂起等待,这种不断尝试获取锁的行为称为自旋

LockSupport

  • 一个工具类,用于线程的阻塞和唤醒操作,类似于wait()和notify()方法,但是更加灵活和可控
  • 提供了park()和unpark()两个静态方法用于线程阻塞和唤醒操作。
  • 优点在于可以在任意时刻阻塞和唤醒线程而不需要事先获取锁或监视器对象。

数据结构之双向链表

  • 双向链表(Doubly Linked List)是一种常见的数据结构,它是由一系列结点(Node)组成的,每个结点包含三个部分:数据域、前驱指针和后继指针。其中,数据域存储结点的数据,前驱指针指向前一个结点,后继指针指向后一个结点。通过这种方式,双向链表可以实现双向遍历和插入、删除操作。

设计模式之模板设计模式

  • 模板设计模式是一种行为型设计模式,定义了一种算法的框架,并将某些步骤延迟到子类中事先,这种设计模式的主要目的是允许子类在不改变算法结构的情况下重新定义算法中的某些步骤。
  • 优点是能够提高代码复用性和可维护性。

2、AQS入门级别理论知识

2.1 是什么?

抽象的队列同步器

AbstractOwnableSynchronizer
AbstractQueuedLongSynchronizer
AbstractQueuedSynchronizer
通常地:AbstractQueuedSynchronizer简称为AQS

技术解释

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

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

2.2 AQS为什么是JUC内容中最重要的基石

2.2.1 和AQS有关的

ReentrantLock

CountDownLatch

ReentrantReadWriteLock

Semaphore

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

锁,面向锁的使用者

定义了程序员和锁交互的使用层API,隐藏了实现细节,你调用即可。

同步器,面向锁的实现者

比如Java并发大神DougLee,提出统一规范并简化了锁的实现,
屏蔽了同步状态管理、阻塞线程排队和通知、唤醒机制等。

3、能干嘛

加锁会导致阻塞

有阻塞就需要排队,实现排队必然需要队列

解释说明

抢到资源的线程直接使用处理业务,抢不到资源的必然涉及一种排队等候机制。抢占资源失败的线程继续去等待(类似银行业务办理窗口都满了,暂时没有受理窗口的顾客只能去候客区排队等候),但等候线程仍然保留获取锁的可能且获取锁流程仍在继续(候客区的顾客也在等着叫号,轮到了再去受理窗口办理业务)。

既然说到了排队等候机制,那么就一定会有某种队列形成,这样的队列是什么数据结构呢?

如果共享资源被占用,就需要一定的阻塞等待唤醒机制来保证锁分配。这个机制主要用的是CLH队列的变体实现的,将暂时获取不到锁的线程加入到队列中,这个队列就是AQS的抽象表现。它将请求共享资源的线程封装成队列的结点(Node),通过CAS、自旋以及LockSupport.park()的方式,维护state变量的状态,使并发达到同步的效果。

4、AQS初步

4.1 AQS初识

官网解释

有阻塞就需要排队,实现排队必然需要队列

AQS使用一个volatile的int类型的成员变量来表示同步状态,通过内置的FIFO队列来完成资源获取的排队工作将每条要去抢占资源的线程封装成一个Node节点来实现锁的分配,通过CAS完成对State值的修改。

4.2 AQS内部体系架构

4.2.1 AQS自身

4.2.1.1 AQS的int变量

AQS的同步状态State成员变量

银行办理业务的受理窗口状态

零就是没人,自由状态可以办理

大于等于1,有人占用窗口,等着去

4.2.1.2 AQS的CLH队列

CLH队列(三个大牛的名字组成),为一个双向队列

银行候客区的等待顾客

4.2.1.3 小总结

有阻塞就需要排队,实现排队必然需要队列

state变量+CLH双端队列

4.2.2 内部类Node(Node类在AQS类内部)

4.2.2.1 Node的int变量

Node的等待状态waitState成员变量

volatile int waitStatus

等候区其它顾客(其它线程)的等待状态

队列中每个排队的个体就是一个

4.2.2.2 Node此类的讲解

内部结构

属性说明

4.3 AQS同步队列的基本结构

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

5. 从我们的ReentrantLock开始解读AQS

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

5.1 ReentrantLock的原理

5.2 从最简单的lock方法开始看看公平和非公平

可以明显看出公平锁与非公平锁的lock()方法唯一的区别就在于公平锁在获取同步状态时多了一个限制条件:hasQueuedPredecessors()

hasQueuedPredecessors是公平锁加锁时判断等待队列中是否存在有效节点的方法

5.3 非公平锁走起,方法lock()

对比公平锁和非公平锁的 tryAcquire()方法的实现代码,其实差别就在于非公平锁获取锁时比公平锁中少了一个判断 !hasQueuedPredecessors()

hasQueuedPredecessors() 中判断了是否需要排队,导致公平锁和非公平锁的差异如下:

公平锁:公平锁讲究先来先到,线程在获取锁时,如果这个锁的等待队列中已经有线程在等待,那么当前线程就会进入等待队列中;

非公平锁:不管是否有等待队列,如果可以获取锁,则立刻占有锁对象。也就是说队列的第一个排队线程在unpark(),之后还是需要竞争锁(存在线程竞争的情况下)

lock()

acquire()

tryAcquire(arg)

下一步:

nonfairTryAcquire(acquires)

如果 return false;, 则继续推进条件,走下一个方法
如果 return true;, 则结束

addWaiter(Node.EXCLUSIVE)

addWaiter(Node mode)

enq(node);

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

假如3号ThreadC线程进来

  • prev
  • compareAndSetTail
  • next

acquireQueued(addWaiter(Node.EXCLUSIVE), arg)

acquireQueued

假如再抢抢失败就会进入

shouldParkAfterFailedAcquire 和 parkAndCheckInterrupt 方法中

shouldParkAfterFailedAcquire

如果前驱节点的 waitStatus 是 SIGNAL状态,即 shouldParkAfterFailedAcquire 方法会返回 true 程序会继续向下执行 parkAndCheckInterrupt 方法,用于将当前线程挂起

parkAndCheckInterrupt

5.4 unlock

sync.release(1);

tryRelease(arg)

unparkSuccessor


13.AbstractQueuedSynchronizer之AQS
http://yuanql.top/2023/06/13/18_JUC/13.AbstractQueuedSynchronizer之AQS/
作者
Qingli Yuan
发布于
2023年6月13日
许可协议