AQS的源码解析(自己的理解)
一:AQS的前置知识
AQS涉及到以下知识点:如不清楚可以去B站上看尚硅谷的大厂面试第二季juc
1.juc
2.LockSupper
3.模板模式
4.CAS
二:AQS是什么
字面意思:
- Abstract Queued Synchronizer(抽象的队列同步器)
实现原理:
- status成员变量+CLH队列变种 FIFO双向虚拟队列
简单流程理解:
通过控制stauts变量来表示锁的状态,0代表资源开发 1代表目前有人占用资源 默认初始化0,抢不到线程的资源就在队列中等待释放资源进行抢占
三:AQS有什么用?用来做什么
那些协作类,它们有很多工作是类似的,所以如果能把实现类似工作的代码给提取出来,变成一个新的底层工具类(或称为框架)的话,就可以直接使用这个工具类来构建上层代码了,而这个工具类其实就是 AQS
有了 AQS 之后,对于 ReentrantLock 和 Semaphore 等线程协作工具类而言,它们就不需要关心这么多的线程调度细节,只需要实现它们各自的设计逻辑即可
简单来说 AQS是用来构建锁或者其他同步组件的基础框架
四:AQS的源码解析
1、 AQS的内部体系关系
下面是精炼出来的内部体系关系内容:省略了很多
//AQS
public abstract class AbstractQueuedSynchronizer{
private transient volatile Node head;//头节点
private transient volatile Node tail;//尾节点
private volatile int state;//资源State变量,默认值为0
private static final Unsafe unsafe = Unsafe.getUnsafe();//直接操作内存的unsafe工具包
static final class Node {
volatile int waitStatus;//线程Node节点状态
volatile Node prev;//前节点
volatile Node next;//后节点
volatile Thread thread;//Node节点的线程
}
}
上面会看到,AQS中会将线程进行包装成一个Node节点,它有前/后节点。这里就会发现他没有一个真正的双向队列,而是通过包装线程为Node类,并指定里面前后的指向,用前/后节点指针指向来实现一个虚拟的双向队列。
- node节点中存放的都是一个个Thread
小知识 map<k,v>的put方法存的是node节点 而node节点中存放k,v
2、 ReentrantLock和AQS的关系
首先我们来看下ReentrantLock的源码
由此看出他内部创建了一个内部类sync sync继承了AQS
3、AQS队列的基本结构
4、通过两个线程抢资源锁来演示
public static void main(String[] args) {
ReentrantLock lock = new ReentrantLock();
new Thread(()->{
try {
lock.lock();
System.out.println("模拟复杂的业务逻辑,20分钟后完成");
try { Thread.sleep(60*1000*20); }catch (InterruptedException e){ e.printStackTrace();}
System.out.println("已执行完毕,释放资源");
} finally {
lock.unlock();
}
},"A");
new Thread(()->{
try {
System.out.println("线程B进行抢占");
lock.lock();
}finally {
lock.unlock();
}
},"B");
}
按照代码的执行顺序,先进入A线程里执行lock.lock()方法,我们看看lock()方法的底层到底做了什么?
① 线程A进入lock方法
进来就会执行内部类sync的lock方法,点进sync的lock中去
映入眼帘的很明显的就是一个抽象方法 它有两个实现类 公平锁FairSync和非公平锁NonfairSync 默认的是非公平锁NonfairSync
因为ReentantLock的无参构造就是NonfairSync 我们继续往下走
线程进来后先进行判断 执行compareAndSetState(0, 1) 比较并交换 第一次status的值是0 那么将会将status的值设置成1并返回true
返回true之后 执行setExclusiveOwnerThread(Thread.currentThread()); 设置占有资源的线程为当前线程
此刻代码已经执行完A线程的lock方法 模拟执行复杂的业务逻辑20分钟,那么该线程将一直持有锁
目前的流程图应该是这样的
②线程B执行lock方法
线程B执行到了这里
判断目前status=0吗 肯定不等于,因为线程A已经将status修改成1 所以走else的 acquire() 方法
㈠acquire方法
这里有三个方法
- tryAcquire()
- addWaiter()
- acquireQueued()
我们先进入tryAcquire()方法中
tryAcquire方法是一个模板设置模式方法 他强制让子类实现该方法 否则就会抛出 UnsupportedOperationException() 异常 由于无参构造是非公平锁NonfairSync ,进入之后就会走nonfairTryAcquire() 方法
final boolean nonfairTryAcquire(int acquires) {//acquires是1
//获取当前线程(当前线程是线程B)
final Thread current = Thread.currentThread();
//获取当前stauts的值(现在status的值是1)
int c = getState();
//status等于0后进入
if (c == 0) {
//从名字就可以看出 又是一个cas方法 将传进来的1和0进行比较并且交换
if (compareAndSetState(0, acquires)) {
//设置占有资源的线程为当前线程
setExclusiveOwnerThread(current);
return true;
}
}
//判断是否是可重入锁
//当前线程是否与占有资源的线程是同一个线程
//当前线程是线程B,占有线程是线程A
//如果线程A由于一些原因再取获取同一把锁将会进入下面逻辑
else if (current == getExclusiveOwnerThread()) {
//如果是可重入锁,那么status的值则累加
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
//当前变量是1 所以返回false
return false;
}
我们回到acquire() 方法中
tryAcquire() 方法返回的是false 但是取反后为true,接着进入addWaiter() 方法中
Node.EXCLUSIVE 的值为null 排他类型
private Node addWaiter(Node mode) {
//将线程B封装成一个node节点
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
//获取队列的尾节点 当前队列中尾节点没有指向任何一个node 那么这个pred肯定是null
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
//尾节点为null 进入enq方法中 将node<线程B>节点加入队列
enq(node);
return node;
}
㈡enq()方法
private Node enq(final Node node) {//node是node<线程B>
for (;;) {//自旋
//获取当前队列中的尾节点 t=null
Node t = tail;
//判断成立 执行下方逻辑
if (t == null) { // Must initialize
//初始化一个空节点 new Node(),作为队列中的哨兵节点
if (compareAndSetHead(new Node())) //cas操作,设置头节点为空节点
tail = head;//尾节点设置为头节点指向,也就是哨兵节点
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
目前队列中将会多出一个哨兵节点,整体状态图为:
由于是 自旋 那么node<线程B>第二次运行的时候
private Node enq(final Node node) {
//第二次进来
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {//就会执行到这里
//将node<线程B>的prev前节点指向t也就是哨兵节点
node.prev = t;
//cas方法 把tail尾节点设置
if (compareAndSetTail(t, node)) {
//将哨兵节点的next后节点指向给node<线程B>
t.next = node;
return t;
}
}
}
}
目前整体状态图为
此时addWaiter() 方法已执行完毕 接下来执行acquireQueued()
㈢acquireQueued() 方法
这个方法最为重要,他的主要作用是将线程抢占不到资源后挂起
final boolean acquireQueued(final Node node, int arg) {//node<线程b> arg=1
boolean failed = true;
try {
boolean interrupted = false;
//自旋
for (;;) {
//获取当前node的前节点 当前线程B的前节点是哨兵节点
final Node p = node.predecessor();
//判断前节点是不是头节点 又执行了一次tryAcquire()方法
//因为tryAcquire会再次抢锁,内容跟上面的一样
//因为线程A一直占用着资源,所以不进入逻辑
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
//p是哨兵节点 node是node<线程B>
//判断此时哨兵节点的waitStatus的值是不是不等于0
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
根据上面代码会进入shouldParkAfterFailedAcquire()方法
㈣shouldParkAfterFailedAcquire()方法
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
//pred是哨兵节点 node是线程B
//获取哨兵节点的ws 默认是0
int ws = pred.waitStatus;
//SIGNAL = -1 条件不成立
if (ws == Node.SIGNAL)
return true;
//ws =0 条件不成立
if (ws > 0) {
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
//cas方法 将哨兵节点的ws设置成-1
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
此时队列的状态图为:
shouldParkAfterFailedAcquire()返回false后 回到了acquireQueued()方法
final boolean acquireQueued(final Node node, int arg) {//node<线程b> arg=1
boolean failed = true;
try {
boolean interrupted = false;
//自旋 第二次进来
for (;;) {
//获取当前node的前节点 当前线程B的前节点是哨兵节点
final Node p = node.predecessor();
//判断前节点是不是头节点 又执行了一次tryAcquire()方法
//因为tryAcquire会再次抢锁,内容跟上面的一样
//因为线程A一直占用着资源,所以不进入逻辑
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
//p是哨兵节点 node是node<线程B>
//判断此时哨兵节点的waitStatus的值是不是不等于0
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
再一次进入shouldParkAfterFailedAcquire()
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
//pred是哨兵节点 node是线程B
//获取哨兵节点的ws 此时的ws值为-1
int ws = pred.waitStatus;
//SIGNAL = -1 条件成立
if (ws == Node.SIGNAL)
//返回true
return true;
if (ws > 0) {
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
又回到acquireQueued方法这里 由于shouldParkAfterFailedAcquire返回是true 条件成立执行parkAndCheckInterrupt方法
㈤parkAndCheckInterrupt() 方法
这个this就是线程B
LockSuppot.park将线程B阻塞到这里 除非触发LockSuppot.unpark方法将线程B唤醒
AQS此时状态图是:
③线程A释放资源
线程A执行完业务逻辑后就会执行 unlock方法
㈠ unlock方法
看源码unlock方法依然是调用sync的release方法
㈡release方法
进来release方法中,先执行if的tryRelease方法
这个方法又是一个标准的模板设计模式跟之前的tryAcquire大同小异
㈢ tryRelease方法
因为我们是通过ReentrantLock进行演示,所以进入下面这个方法中
protected final boolean tryRelease(int releases) { //releases = 1
//getState获取当前的stauts 值 =1
int c = getState() - releases;
//判断当前线程是否是资源占用线程
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
//根据上面的减法我们知道 c =0 进入以下逻辑
if (c == 0) {
free = true;
//将当前占用资源的线程改为null
setExclusiveOwnerThread(null);
}
//设置status的值为0
setState(c);
return free;
}
当前全局状态图:
我们再回到release方法中
public final boolean release(int arg) {
//返回为true
if (tryRelease(arg)) {
//取出头节点 也就是哨兵节点
Node h = head;
//哨兵节点不等于null,并且他的ws是-1 条件不成立
if (h != null && h.waitStatus != 0)
//唤醒阻塞的线程
unparkSuccessor(h);
return true;
}
return false;
}
㈣unparkSuccessor方法
private void unparkSuccessor(Node node) {//哨兵节点
//获取哨兵节点的ws ws = -1
int ws = node.waitStatus;
//ws = -1 条件成立
if (ws < 0)
//cas方法 将哨兵节点ws的值设置成0
compareAndSetWaitStatus(node, ws, 0);
//获取哨兵节点的后一个节点 也就是线程B
Node s = node.next;
//线程B不为空 ws值是0 条件不成立
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
//进入
if (s != null)
//将线程B唤醒
LockSupport.unpark(s.thread);
}
LockSupport.unpark(s.thread)把线程B给唤醒 来进行抢占资源
此时状态图为
④线程B被唤醒,抢占资源
由于上面线程b被park阻塞在acquireQueued这个方法中
我们再来看看acquireQueued方法
㈠acquireQueued方法
线程b在自旋死循环,现在被线程A唤醒,进行抢占资源
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
//线程B被唤醒 又进入循环
for (;;) {
//获取线程B的前节点,也就是哨兵节点
final Node p = node.predecessor();
//哨兵节点是头节点,
//再次执行tryAcquire抢占资源,tryAcquire返回true
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
㈡tryAcquire方法
上面讲过tryAcquire是一个模板模式方法 所以我们进入他的实现方法nonfairTryAcquire方法中
final boolean nonfairTryAcquire(int acquires) {
//拿到线程B
final Thread current = Thread.currentThread();
//获取成员变量status的值 stauts = 0
int c = getState();
//条件成立
if (c == 0) {
//cas 比较并交换 将stauts设置成1
if (compareAndSetState(0, acquires)) {
//占有资源的线程设置成线程B
setExclusiveOwnerThread(current);
//返回true
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
此刻返回B之后我们再次进入acquireQueued方法中
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
//执行tryAcquire方法 返回了true
if (p == head && tryAcquire(arg)) {
//线程B设置成头节点
setHead(node);
//将哨兵节点的后节点的指向设置成NUll
//这样的话,哨兵节点就没有任何指向了 等待GC回收
p.next = null; // help GC
failed = false;
返回是否被中断
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
㈢setHead方法
private void setHead(Node node) {
//线程B设置成头节点
head = node;
//node<线程B> = node<null>
node.thread = null;
//线程B的前节点设置成空
node.prev = null;
}
目前全局状态图为
以此循环,如果有别的线程加入队列也是维持以上的流转操作。
五:AQS总结
上面的流程就是通过ReentrantLock来演示AQS抢占资源和释放资源的过程。
AQS的流程用一句话来解释就是:通过资源变量State+AQS的同步管理队列,实现了线程抢占资源的管理
用成员变量 status 来控制 此时资源是否有线程抢占,如果是0就是没有线程占有资源
如果大于等于1 就是资源已经被占用
没有占用资源的线程就放入队列中进入等候,通过阻塞线程和释放线程来控制队列中的线程。