🎈 简介
是一个锁框架,各大显示锁和 JUC 里面一些工具都是集成 AQS 实现的
全称 AbstractQueuedSynchronizer -->简称同步锁框架
-
用到了模板模式 tryacquire()realse() 等方法需要子类覆写 / 抛了 sup 异常的都是要子类覆写的
-
一般的 lock 类都是实现 Lock 接口的,而内部会继承 AQS,因为 Lock 是面向使用者的,而 AQS 这个框架是锁的实现框架,使用者一般不会关心 AQS
-
主要用于解决锁分配给“谁”的问题,整体就是一个抽象的 FIFO 队列来完成资源获取线程的排队工作,并通过一个 int 类变量表示持有锁的 zhua
🎈 CLH 队列(FIFO)
-
CLH 锁其实就是一种基于队列(具体为单向链表)排队的自旋锁, 由于是 Craig、Landin 和 Hagersten 三人一起发明的,因此被命名为 CLH 锁,也叫 CLH 队列锁
-
简单的 CLH 锁可以基于单向链表实现,申请加锁的线程首先会通过 CAS 操作在单向链表的尾部增加一个节点,之后该线程只需要在其前驱节点上进行普通自旋,等待前驱节点释放锁即可。
-
由于 CLH 锁只有在节点入队时进行一下 CAS 的操作,在节点加入队列之后,抢锁线程不需要 进行 CAS 自旋,只需普通自旋即可。因此,在争用激烈的场景下,CLH 锁能大大减少 CAS 操作的数量,以避免 CPU 的总线风暴
-
声明一个 node 节点,locked 是自身,myPred 是前一个 node 节点的引用(组成一个单向链表),因为获取锁的线程,可能是多个,后节点会不断的自旋(普通自旋)自己的 myPred 指向的前一个 node 的 Locked 属性,当这个属性探测得到是 false 的时候,说明前驱节点已经释放了锁,自己应该去抢锁了
🎈 State
使用 volatile 修饰的,表示资源的状态(分独占模式和共享模式),子类需要定义如何维护这个状态,控制如何获取锁和释放锁。
-
初始值=0 表示没有线程抢占,1=锁已经被抢占(抢占之后调用 unLock()方法会释放锁,将 state=1 重置为 0) / 大于 1 的情况,说明是可重入锁,举例子:该值等于 3,说明已经被重入了 3 次,最后递减 state 的时候也应该递减 3 次,重置为 0 的时候表示已经释放了锁
-
getState-获取 state 状态
-
setState-设置 state 状态
-
compareAndSetState-乐观锁机制设置 state 状态
-
独占模式是只有一个线程能够访问资源,而共享模式可以允许多个线程访问资源
🎈 Node 节点
-
Thread : 当前线程
-
Prev : 上个节点
-
Next : 下个节点
-
Head node : 头节点,指向队列第一个节点
-
Tail node : 尾节点,指向队列最后一个节点
-
predecessor()方法 : 返回上一个节点
🎈 AQS 结构
🎈 AQS 内部结构
🎈 AQS 的执行流程
CAS 尝试获取锁,失败进入 acquire
双向链表中,第一个节点为虚节点(也叫哨兵节点),其实并不存储任何信息,只是占位。真正的第一个有数据的节点是从第二个节点开始的
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
1、tryAcquire(尝试抢占)
tryAcquire 尝试获取锁,主要有四种情况
-
锁已经被其他线程获取;
-
锁没有被其他线程获取,但是需要排队;
-
cas 失败(可能过程中其他线程获取到锁);
-
获取到锁;
可以明显的看出公平锁和非公平锁本质区别就是多了一个限制条件hasQueuedPredecessors()
主要判定当前线程前面是否有其他线程正在排队,有则表示当前线程需要排队
流程:
-
先获取 state,判断是否=0,判断是否需要排队,尝试 CAS,成功设置持有锁的线程 id 为当前线程
-
判断当前线程是否持有锁线程(可重入)
2、addWaiter(入队)
将当前线程封装成 Node 对象,并加入排队队列中
/**
* Creates and enqueues node for current thread and given mode.
*
* @param mode Node.EXCLUSIVE for exclusive, Node.SHARED for shared
* @return the new node
*/
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
//获取队列
Node pred = tail;
//队列不等于null 已经初始化了
if (pred != null) {
//将新的node加入排队队列末尾即可
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
//表示队列为空,需要执行队列初始化
enq(node);
return node;
}
//执行队列初始化
private Node enq(final Node node) {
//自旋锁
for (;;) {
//获取当前队列
Node t = tail;
if (t == null) { // Must initialize
//CAS初始化一个空的Node,作为哨兵节点
if (compareAndSetHead(new Node()))
//作为排队队列的head
tail = head;
} else {
//当前节点的prev指向哨兵节点
node.prev = t;
//CAS将当前节点设置成队列的尾节点
if (compareAndSetTail(t, node)) {
//哨兵节点的next指向当前节点
t.next = node;
return t;
}
}
}
}
3、acquireQueued(唤醒、通知稳定在队列中)
final boolean acquireQueued(final Node node, int arg) {
//标识节点可能取消排队
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
//获取当前节点的前辈节点
final Node p = node.predecessor();
/**
* 判断前辈节点是否是head,如果是,说明它是下一个可以获得锁的线程则调用一次
* tryAcquire,尝试获取锁,若获取到,则将链表的关系重新维护
*/
if (p == head && tryAcquire(arg)) {
//设置头节点为当前节点
setHead(node);
//将前辈节点置为null,从队列中移除
p.next = null; // help GC
//正常结束,不需要取消排队
failed = false;
return interrupted;
}
/**
* 如果前辈节点不是head,或者获取锁失败,在判断其前辈节点的waitStatus,是不是SIGNAL
* 如果是则当前线程调用park,进入阻塞状态。
* 如果不是需要再次循环上面的逻辑在执行一遍
*/
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
//获取前辈节点的waitStatus
int ws = pred.waitStatus;
//如果waitStatus等于-1 准备状态,等着唤醒,直接返回
if (ws == Node.SIGNAL)
/*
* This node has already set status asking a release
* to signal it, so it can safely park.
*/
return true;
//表示前辈节点已经被移除
if (ws > 0) {
/*
* Predecessor was cancelled. Skip over predecessors and
* indicate retry.
*/
do {
//取消前辈节点,从列表移除,重新维护列表关系
// 将前任的前任 赋值给 当前的前任
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
// 将前任的前任的 next 赋值为 当前节点
pred.next = node;
} else {
/*
* waitStatus must be 0 or PROPAGATE. Indicate that we
* need a signal, but don't park yet. Caller will need to
* retry to make sure it cannot acquire before parking.
*/
//CAS将前辈节点的waitStatus设置成-1
//希望自己的上一个节点在释放锁的时候,通知自己(让自己获取锁)
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
private final boolean parkAndCheckInterrupt() {
//线程挂起,不会在继续执行
LockSupport.park(this);
//再次调用unpark继续执行
return Thread.interrupted();
}
private void setHead(Node node) {
//当前节点设置为头结点
head = node;
//将当前节点线程设置null
node.thread = null;
//将当前节点的prev设置null
node.prev = null;
}
4、release
public final boolean release(int arg) {
//此处会返回true,如果重入锁会返回false
if (tryRelease(arg)) {
//获取头结点
Node h = head;
// 所有的节点在将自己挂起之前,都会将前置节点设置成 SIGNAL,希望前置节点释放的时候,唤醒自己。
// 如果前置节点是 0 ,说明前置节点已经释放过了。不能重复释放了,后面将会看到释放后会将 ws 修改成0.
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
//证明释放锁了
if (c == 0) {
free = true;
//将持有锁的线程id设置null
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
//此时传入的节点是头结点
private void unparkSuccessor(Node node) {
/*
* If status is negative (i.e., possibly needing signal) try
* to clear in anticipation of signalling. It is OK if this
* fails or if status is changed by waiting thread.
*/
int ws = node.waitStatus;
if (ws < 0)
// 将 head 节点的 ws 改成 0,清除信号。表示,他已经释放过了。不能重复释放。
compareAndSetWaitStatus(node, ws, 0);
/*
* Thread to unpark is held in successor, which is normally
* just the next node. But if cancelled or apparently null,
* traverse backwards from tail to find the actual
* non-cancelled successor.
*/
// 如果 next 是 null,或者 next 被取消了。就从 tail 开始向上找节点。
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
/*
* 从尾部开始,向前寻找未被取消的节点,直到这个节点是 null,或者是 head。
* 也就是说,如果 head 的 next 是 null,那么就从尾部开始寻找,直到不是 null 为止,找到这个 head 就不管了。
* 如果是 head 的 next 不是 null,但是被取消了,那这个节点也会被略过。
*/
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
/*
* 唤醒 head.next 这个节点。
* 通常这个节点是 head 的 next。
* 但如果 head.next 被取消了,就会从尾部开始找。
*/
if (s != null)
LockSupport.unpark(s.thread);
}
🎈 LockSupport
1、作用
-
阻塞指定线程: park()
-
唤醒指定线程: unpark()
-
构建同步组件的基础工具
❗️注意点: 方法基本是 Unsafe 类实现的,native 本地方法,因为线程相关操作大部分都需要通过操作系统的配合才能完成,而 Java 并不能直接和操作系统打交道,只能通过 native 本地的 c++方法去打交道,而 sun 公司提供了一个类 Unsafe,里面可以通过调用 native 方法去间接和 OS 打交道,相当于 unsafe 类就是 java 平台的一个后门;
2、常见的方式
-
先 park(),再 unpark(),线程会阻塞,unpark 后继续执行。
-
先 unpark(),再 park(),线程不会阻塞,park 后仍然可以继续执行。
3、实现原理
-
这两个方法的底层都是 native 的,我们看不见实现过程,所以下面直接归纳整理出原理及设计到的变量,理解就好。
-
在底层实现中,每个线程都有自己的一个 Parker 对象,由三部分组成 _counter , _cond 和 _mutex。
-
简单描述下:_counter 就是是否阻塞的标记 , _cond 就好比阻塞后线程的容器, _mutex 是互斥锁,线程需持有后进入_cond 中。
4、park()流程
-
执行 park()方法
-
检查_counter 是否是 0
-
如果_counter 是 0,则获取互斥锁_mutex
-
获取到互斥锁,进入_cond 中进行阻塞
-
再次设置_counter 为 0
5、unpark()流程
-
执行 unpark(thread-1),设置_counter=1
-
唤醒_cond 中阻塞的线程 thread-1
-
线程 thread-1 恢复运行
-
再次设置_counter = 0
🎈 AQS 底层在唤醒等待线程的时候要从队尾往前遍历
可以从源码看出,unpark 的线程保存在后继节点中,通常是下一个节点。但是如果被取消或明显为 null,则从 tail 向后遍历以找到实际未取消的后继节点
首先需要知道 AQS 中,线程入队时的指针变动操作。
-
将线程包装为 node,将该节点 prev 前指针指向 tail 节点。
-
将 tail 标记指向新创建的节点。(入队)
-
将旧的 tail 节点 next 指针指向新的节点。
至此完成新增节点入队闭环。
极端并发的情况下,如果从前向后查找需要唤醒的线程节点时,有新节点入队且只进行到 2 步骤,那么此时前后遍历只能索引到倒数第二个节点,然后出现断层,漏掉了新节点。
如果从后往前查找,可以完整遍历完所有节点。
🎈 进入队列前还尝试去做一次 CAS 抢锁
抢不到锁的节点, 进入等待队列前, 会判断自己前一个节点是否是头节点, 如果是, 说明自己是第一个进队列的人, 就尝试 CAS 抢锁
因为并发编程是其他线程也在同步并行, 在这个过程中, 前面拿到锁的人, 很可能已经释放过锁, 我这个时候尝试 CAS 抢锁, 成功性非常大, 充分考虑每个非原子性的操作中间别的线程在做什么, 我能做什么优化空间 , 因为并发编程, 并不是时时刻刻在往死里并发 就算你用户量很大, 其实线程基本是交替执行的, 这种小动作, 做的贡献是最有效的 , 这是早期的 sync 不关心的, 所以 AQS 的开创性意义比较大, 引发了后面 sync 的锁优化升级迭代
🎈 AQS 为什么用双向链表,(为啥不用单向链表)
因为 AQS 中,存在取消节点的操作,如果使用双向链表只需要两步
-
需要将 prev 节点的 next 指针,指向 next 节点。
-
需要将 next 节点的 prev 指针,指向 prev 节点。
但是如果是单向链表,需要遍历整个单向链表才能完成的上述的操作。比较浪费资源。
🎈 为什么要创建一个虚拟节点呢
事情要从 Node 类的 waitStatus 变量说起,简称 ws。每个节点都有一个 ws 变量,用于这个节点状态的一些标志。初始状态是 0。如果被取消了,节点就是 1,那么他就会被 AQS 清理。还有一个重要的状态:SIGNAL —— -1,表示:当当前节点释放锁的时候,需要唤醒下一个节点。所有,每个节点在休眠前,都需要将前置节点的 ws 设置成 SIGNAL。否则自己永远无法被唤醒。
❓为什么需要这么一个 ws :
防止重复操作。假设,当一个节点已经被释放了,而此时另一个线程不知道,再次释放。这时候就错误了。所以,需要一个变量来保证这个节点的状态。而且修改这个节点,必须通过 CAS 操作保证线程安全。
❓为什么要创建一个虚拟节点呢?
每个节点都必须设置前置节点的 ws 状态为 SIGNAL,所以必须要一个前置节点,而这个前置节点,实际上就是当前持有锁的节点。那第一个节点怎么办?他是没有前置节点的。那就创建一个假的
总结:
每个节点都需要设置前置节点的 ws 状态(这个状态为是为了保证数据一致性),而第一个节点是没有前置节点的,所以需要创建一个虚拟节点。