前言
在多线程下,如果被请求的共享资源空闲,则将当前请求资源的线程设为有效的工作线程,并将共享资源设为锁定状态,如果被请求的共享资源被占用,那么就需要一套阻塞等待以及被唤醒时锁分配的机制,这就是AQS的核心思想
一、AQS是什么?
AQS 是 Java 并发包中的一个关键组件,用于构建锁和其他同步组件的基础框架。
它提供了一种实现同步器(如锁和其他同步工具)的框架,被许多同步类所依赖,例如 ReentrantLock、Semaphore 等。
AQS 的核心思想是基于 FIFO 队列 实现线程的阻塞和唤醒,以及维护同步状态。
二、AQS的重要成员变量
1.源码检视
//用于构造fifo队列的node内部类
static final class Node
//头节点
private transient volatile Node head;
//尾节点
private transient volatile Node tail;
//同步状态,后面会详细讲解
private volatile int state;
//旋转速度快于使用定时停留的纳秒数。粗略的估计足以以非常短的超时来提高响应性。
static final long spinForTimeoutThreshold = 1000L;
//用来调用jvm的api的unsafe类
private static final Unsafe unsafe = Unsafe.getUnsafe();
2.state
AQS主要维护了一个表示锁状态的变量。它表示有多线程获取了锁
- 通过CAS操作实现队state的修改
3.等待队列
AQS维护了一个可阻塞的等待队列,也叫CLH队列
- 队列的头节点和尾节点分别表示对头和队尾
- CLH队列由链表实现,以自选的方式获取资源,是可阻塞的先进先出的双向队列
三、获取资源与释放资源
获取资源的方法有acquire()和aquiredShared()
acquire() 用于独占模式获取资源,它会自旋直到成功获取资源或被中断。
addWaiter() 将当前线程包装成一个节点并插入队尾,返回在等待队列中的节点。
private boolean doAcquireNanos(long arg, long nanosTimeout)
throws InterruptedException {
//判断设置的超时时间是否正确
//如果不正确,直接返回false
//那么上一层的tryAcquirenano方法整体就会返回false
//那么就不可以继续执行下去了
if (nanosTimeout <= 0L)
return false;
//计算获取锁的限制时间
//超过这个时间就不可以获取了
//System.nanoTime是一个本地方法,用来获取虚拟机时间的,精确到纳秒级别
//所以传进来的nanosTimeout必须为纳秒级别
// 1秒 = 10^9纳秒,十亿份之一
//这也是为什么使用long类型
final long deadline = System.nanoTime() + nanosTimeout;
//因为没抢到锁,所以要将该线程加入到队列里面
//并且nextWaiter为Exclusive,代表该结点线程状态是独占式的
final Node node = addWaiter(Node.EXCLUSIVE);
//failed变量记录过程是否出错
boolean failed = true;
try {
//死循环
for (;;) {
//下面这段代码就是自旋获取锁的逻辑
//获取上一个线程
final Node p = node.predecessor();
//从下面这个判断可以判断出,这个抢锁过程是公平的
//如果上一个线程是头结点
//并且自己尝试获取锁成功
if (p == head && tryAcquire(arg)) {
//那么该线程就可以继续运行了
//先将头结点设为自己
setHead(node);
//断开与头结点的连接
//让头结点可以回收
p.next = null; // help GC
//failed为false代表自旋过程无出错
failed = false;
//自旋结束,返回true,回到上一层继续运行
return true;
}
//上一个不是头结点,或者虽然是头结点但抢不到锁
//计算还需要等待多长时间
nanosTimeout = deadline - System.nanoTime();
//如果没有时间剩余了
if (nanosTimeout <= 0L)
//返回false
//上一层处理
return false;
//还有时间剩余,看需不需要park
//这里一样是自旋两次,然后就会返回true,代表需要Park
//继续判断剩余时间是否足够进行park
//因为如果park需要的时间都要多于线程剩下的时间
//没什么必要park了,让其再自旋多一次,拿不到就超时返回false
if (shouldParkAfterFailedAcquire(p, node) &&
nanosTimeout > spinForTimeoutThreshold)
//如果超时了,而且需要park,那就park掉
//这里park掉,线程也是会继续进行倒计时的
//在这段时间内都会被park掉,这段时间内,只有unpark和interrupt可以恢复
//超过这个时间也会恢复,然后又一次自旋,然后超时GG
LockSupport.parkNanos(this, nanosTimeout);
//判断唤醒该线程的方式是不是被别的线程interrupt
//因为park是不会覆盖interrupt标志位的
if (Thread.interrupted())
//如果是被中止
//抛出异常
throw new InterruptedException();
}
} finally {
//同理,没有修改过failed变量,抛出异常,超时
//就会执行下面cancelAcquire
if (failed)
cancelAcquire(node);
}
}
释放资源:
private void cancelAcquire(Node node) {
//判断要移除的线程在不在
if (node == null)
//不在就直接返回
return;
//将结点里面的线程删除
node.thread = null;
// Skip cancelled predecessors
//获取结点的上一个结点
//下面就是针对前面的一些被取消的结点的操作
//这些结点也是要被删除的
Node pred = node.prev;
//循环判断上一个结点的状态是否大于0,如果大于0代表就要被删除
while (pred.waitStatus > 0)
//进入到这里就判断上一个结点要删除了
//所以让Node与上上个结点连接,
//同时将pred变为上上个结点,为了下一次循环
//这一步可以视为两步
//pred = pred.prev
//node.prev = pred
node.prev = pred = pred.prev;
//循环在这里就结束了,当初就是在这里卡了好久
//现在已经让正常的结点node.prev为正常的结点
//但还没有进行断开,下面的操作就是断开的
//pred此时就是正常的结点
//predNext就是前面一个被取消的结点
//下面只需要对这个predNext删除就行,因为这段的线程都是取消的
Node predNext = pred.next;
//将线程状态改为cancelled
node.waitStatus = Node.CANCELLED;
//如果线程是尾结点
//那么就CAS将尾结点换成Pred,也就是此时尾结点变成了pred
if (node == tail && compareAndSetTail(node, pred)) {
//然后CAS将pred后面的线程全部删除,改为Null
//这里就完全断开了需要删除的结点,并且正常的结点成为了尾结点
compareAndSetNext(pred, predNext, null);
}
//下面这一步,针对node不是尾结点
//或者node是尾结点,但在替换过程中,新插入了结点
else {
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)
//让正常结点指向node的后驱结点
//即断开了node
compareAndSetNext(pred, predNext, next);
}
//如果是正常结点是头结点,
//那就直接唤醒后面的线程
else {
unparkSuccessor(node);
}
node.next = node; // help GC
}
}