开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 6 天,点击查看活动详情
觉得对你有益的小伙伴记得点个赞+关注
后续完整内容持续更新中
希望一起交流的欢迎发邮件至javalyhn@163.com
1. 什么是AbstractQueuedSynchronizer
1.1 字面意思
通常地:AbstractQueuedSynchronizer简称为AQS
抽象的队列同步器
1.2 技术解释
是用来构建锁或者其它同步器组件的重量级基础框架
及整个JUC体系的基石
,
通过内置的FIFO队列
来完成资源获取线程的排队工作,并通过一个int类变量 表示持有锁的状态
CLH:Craig、Landin and Hagersten 队列,是一个单向链表,AQS中的队列是CLH变体的虚拟双向队列FIFO
2. AQS为什么是JUC内容中最重要的基石
2.1 和AQS有关的内容
ReentrantLock
CountDownLatch
ReentrantReadWriteLock
Semaphore
2.2 锁和同步器的关系
锁是面向锁的使用者
,定义了程序员和锁交互的使用层API,隐藏了实现细节,调用即可
同步器是面向锁的实现者
,比如Java并发大神DougLee,提出统一规范并简化了锁的实现,
屏蔽了同步状态管理、阻塞线程排队和通知、唤醒机制等。
3. AQS的作用
加锁会导致阻塞
,有阻塞就需要排队,实现排队必然需要队列
抢到资源的线程直接使用处理业务,抢不到资源的必然涉及一种排队等候机制
。抢占资源失败的线程继续去等待(类似银行业务办理窗口都满了,暂时没有受理窗口的顾客只能去候客区排队等候
),但等候线程仍然保留获取锁的可能且获取锁流程仍在继续(候客区的顾客也在等着叫号,轮到了再去受理窗口办理业务)。
既然说到了排队等候机制
,那么就一定会有某种队列形成,这样的队列是什么数据结构呢?
如果共享资源被占用,就需要一定的阻塞等待唤醒机制来保证锁分配
。这个机制主要用的是CLH队列的变体实现的,将暂时获取不到锁的线程加入到队列中,这个队列就是AQS的抽象表现。它将请求共享资源的线程封装成队列的结点(Node)
,通过CAS、自旋以及LockSupport.park()的方式,维护state变量的状态,使并发达到同步的效果。
4. 初步理解AQS
我们看一下官方解释
有阻塞就需要排队,实现排队必然需要队列
AQS使用一个
volatile的int类型的成员变量来表示同步状态
,通过内置的FIFO队列
来完成资源获取的排队工作将每条要去抢占资源的线程
封装成 一个Node节点
来实现锁的分配,通过CAS完成对State值的修改。
5. AQS内部体系架构
5.1 AQS自身
AQS的int变量
:这是AQS的同步状态State成员变量
好比银行业务的受理窗口,假设如果是0,就是自由状态可以办理;大于等于1就说明有人正在占用窗口,需要等待。
AQS的CLH队列
:CLH队列(三个大牛的名字组成),为一个双向队列
好比银行候客区的等待顾客
小总结:有阻塞就需要排队,要排队必然需要队列 (state变量+CLH双向队列)
5.2 内部类Node(在AQS类内部)
Node的int变量
:Node的等待状态waitState成员变量
Node的int变量和AQS自身的int变量完全是两码事!!!
好比银行排队等待区其他顾客(其他线程)的等待状态,队列中每一个排队的个体就是Node
Node
的内部结构以及属性说明
6. AQS同步队列基本结构
CLH:Craig、Landin and Hagersten 队列,是个单向链表,AQS中的队列是CLH变体的虚拟双向队列(FIFO)
7. 从ReentrantLock开始解读AQS
7.1 Lock接口
Lock接口的实现类,基本都是通过【聚合】了一个【队列同步器】的子类
完成线程访问控制的
7.2 ReentrantLock的原理
7.3 从lock方法看公平与非公平
7.4 公平锁与非公平锁在源码上有什么区别
可以明显看出公平锁与非公平锁的lock()方法唯一的区别就在于公平锁在获取同步状态时多了一个限制条件: hasQueuedPredecessors()
hasQueuedPredecessors是公平锁加锁时判断等待队列中是否存在有效节点的方法
这里我先讲非公平锁,以非公平锁为突破口,hasQueuedPredecessors()这个方法最后讲(不难)
8. 非公平锁lock() 源码详解
8.1 非公平锁走起,方法lock()
对比公平锁和非公平锁的 tryAcquire()方法的实现代码,其实差别就在于非公平锁获取锁时比公平锁中少了一个判断 !hasQueuedPredecessors()
hasQueuedPredecessors() 中判断了是否需要排队
,导致公平锁和非公平锁的差异如下:
公平锁
:公平锁讲究先来先到,线程在获取锁时,如果这个锁的等待队列中已经有线程在等待,那么当前线程就会进入等待队列中;
非公平锁
:不管是否有等待队列,如果可以获取锁,则立刻占有锁对象。也就是说队列的第一个排队线程在unpark(),之后还是需要竞争锁(存在线程竞争的情况下)
8.2 lock()
8.3 acquire()
acquire()有三大流程
- tryAcquire()
- addWaiter()
- acquireQueued
8.4 tryAcquire(arg)
该函数的作用是尝试获得一个许可
,对于AQS来说这是一个没有实现的抽象类,他的实现交给子类,
该函数有两个返回值
true 结束
false 继续推执行下一个方法
final boolean nonfairTryAcquire(int acquires) {
//获取当前线程
final Thread current = Thread.currentThread();
//拿到state状态值(返回AQS自身的int变量值)
int c = getState();
//如果是0就表示没有其他线程持有锁,则当前线程有机会对它赋值
if (c == 0) {
// 如果赋值成功,则表示当前线程持有锁(此处采用CAS)
// 对state加1,表示当前线程第一次持有锁,注意acquires=1
if (compareAndSetState(0, acquires)) {
// 将当前线程赋值到exclusiveOwnerThread字段
// 其他地方可以通过该字段判断是哪个线程在持有锁
setExclusiveOwnerThread(current);
//返回true,表示加锁成功
return true;
}
}
// 如果不是0,则说明有线程占用锁了,那么判断占有锁的线程是不是
// 当前线程,如果是,则可重入(ReentrantLock)
else if (current == getExclusiveOwnerThread()) {
// 因为再次持有锁(重入进来的),所以此处对state赋值
// state是几,就表示当前是第几次再次获得锁
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
// 如果以上都不是,则没有获取到锁
return false;
}
8.5 addWaiter(Node.EXCLUSIVE)
如果没有获取到锁就会走到这一步
这个方法的注释: 创建一个入队node为当前线程,Node.EXCLUSIVE是独占锁,Node.SHARED是共享锁
private Node addWaiter(Node mode) {
//创建一个入队node为当前线程
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
// 尝试enq的快速路径;故障时备份到完整enq
Node pred = tail;
// 如果pred(尾节点)不为空
if (pred != null) {
// 将node的前驱结点prev设置为pred
node.prev = pred;
// 用CAS设置当前node为尾结点
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
// 不存在tail节点,进入该方法
enq(node);
return node;
}
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
// 首次没有尾结点,必须要初始化一个首节点(哨兵节点,仅仅用来占位,没有任何数据)
if (compareAndSetHead(new Node()))
tail = head;
// 进入到else说明已经初始化过,就往队列后添加节点
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
双向链表中,第一个节点为
虚节点(也叫哨兵节点)
,其实并不存储任何信息,只是占位。
真正的第一个有数据的节点,是从第二个节点开始的。
8.6 acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
当addWaiter插入节点后,调用该方法进行阻塞
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
// 如果node是第二个节点,并且线程当前持有锁
// 那么就将node变成第一个节点(head节点),源head直接GC回收掉
// 就是将队列中的每一个元素往前挪动1个位置
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
// 如果没有获取到锁,则调用LockSupport.park方法挂起,并放到队列的最后一个(tail)位置
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
// 获取前驱结点的状态
int ws = pred.waitStatus;
// 如果是SIGNAL状态,及等待被占用的资源释放,直接返回true
// 准备继续调用parkAndCheckInterrupt
if (ws == Node.SIGNAL)
return true;
// ws大于0说明是CANCELLED状态
if (ws > 0) {
// 循环判断前驱结点的前驱结点是否也为CANCELLED状态,忽略该状态的节点,重新连接队列
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
// 将当前节点的前驱结点设置为SIGNAL状态,用于后续被唤醒操作
// 程序第一次执行到这里返回false,还会进行第二次循环,最终从代码第七行返回
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
如果前驱节点的 waitStatus 是
SIGNAL状态
,即 shouldParkAfterFailedAcquire 方法会返回 true 程序会继续向下执行parkAndCheckInterrupt 方法
,用于将当前线程挂起
private final boolean parkAndCheckInterrupt() {
// 线程挂起,不会再向下执行
LockSupport.park(this);
// 根据park方法API描述,程序在下面三种情况会继续向下执行
// 1 被unpack
// 2 被中断(interrupt)
// 3 其他不合逻辑的返回才会继续向下执行
// 因上述三种情况执行至此,返回当前线程的中断状态,并且清空中断标志
// 如果由于被中断,该方法会返回true
return Thread.interrupted();
}
8.7 公平锁lock()的hasQueuedPredecessors()
public final boolean hasQueuedPredecessors() {
Node h, s;
// 如果头节点不为空
if ((h = head) != null) {
// 如果第2个节点为空,或者第2个节点不为空但是取消了等待(>0)
if ((s = h.next) == null || s.waitStatus > 0) {
s = null; // traverse in case of concurrent cancellation
//从尾部向头部遍历,找到离头部最近的,并且waitStatus<=0的节点,用s保存这个节点
for (Node p = tail; p != h && p != null; p = p.prev) {
if (p.waitStatus <= 0)
s = p;
}
}
// 如果s不是空并且s中的线程不是当前线程,则说明s中保存的线程正在等待获取锁,所以返回true,表示有其他线程等待
if (s != null && s.thread != Thread.currentThread())
return true;
}
// 如果头节点为空,说明队列里没任何线程,进一步说明没有其他线程在等待,直接返回false
return false;
}
9. 建议
AQS还是有难度的,需要多复习!