文章目录
让我们来聊聊AQS
AQS框架是在Java并发中很重要的一个框架,CountDownLatch、ReentrantLock、Semaphore、ReentrantReadWriteLock的重要的内部类都主要是在AQS上调用功能,从而达到并发工具的要求。
那AQS的全称是什么呢?AbstractQueuedSynchronizer,中文翻译也就是抽象的队列同步器,专门抽象出来给所有并发工具类使用的一个抽象类。
一、学习AQS的前提
AQS里面的核心是state的控制和LockSupport类的API支持,state我们先不说,等下分析源码的时候我们可以知道state是怎么被控制的以及状态的反转。
LockSupport是什么?和我们用的Lock和Synchronized关键字、Lock有什么区别
上代码
//这行代码会抛出一个异常,因为Lock的执行顺序必须先上锁才可以进行unlock操作。
public void testLock(){
Lock lock = new ReentrantLock();
new Thread(()->{
try {
Thread.sleep(50000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//在这里进行lock
lock.lock();
}, "A").start();
new Thread(()->{
lock.unlock();
},"B").start();
}
//然而用LockSupport的时候并没有发生任何错误
public void testLockSupport(){
Thread A = new Thread(()->{
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//在这里进行lock
LockSupport.park();
//LockSupport.park();
System.out.println("I'm coming");
}, "A");
A.start();
new Thread(()->{
LockSupport.unpark(A);
//我们可以测试一下知道,即使我解锁了两次,一样不影响得到想要的结果
//LockSupport.unpark(A);
System.out.println("I'm leaving");
},"B").start();
}
看到park和unpark的简述我们可以知道,**“许可”**是一个仅有一个的通行证,unpark会为它加一,park会为为它减一,但是也只有一个通行证。
好了,到这里我们也就大致了解了LockSupport提供了怎么样的支持,接下来一起来看看AQS怎么看源码吧。
二、AQS源码解析
因为ReentrantLock用AQS写,我们可以Debug一下它来看一下他的lock方法和unlock方法是怎么用。
可以看到在ReentrantLock中有一个抽象类继承了AQS,我们主要看他的非公平锁实现,也就是NonfairSync
重要的数据结构AQS里的Node构造一个双向链表,读者自己去看一下源码的简单解析,再回来探讨。
2.1 acquire()方法详解
//获取锁,传入的arg = 1
public final void acquire(int arg) {
//这里的大致操作就是
//1、先尝试去获取锁,获取成功直接返回,不成功跳到2;
//2、尝试构造一个Node节点加入队列
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
//这时进来说明线程没有获取锁成功,并被jvm判断到线程已经中断了,线程再进行自我中断
selfInterrupt();
}
2.2 nonfairTryAcquire()方法详解
//调用传进来的参数 acquires是1,即获取一个许可即可
final boolean nonfairTryAcquire(int acquires) {
//获取当前线程,为阻塞线程或者允许线程做准备
final Thread current = Thread.currentThread();
//这个状态就是 AQS中的state,在ReentrantLock使用它的语义中,state >= 1时有线程在使用,state = 0时无线程使用
int c = getState();
//通过CAS抢占
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
//表示当前占用这个锁的线程又重新入了锁,加状态给他,设置好state值
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;
}
2.3 addWaiter()方法详解
private Node addWaiter(Node mode) {
//为加入队列构造一个新节点,mode为独占模式,即EXCLUSIVE,Node还有一个模式为SHARE,即共享模式
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;
//如果链表已经初始化了,直接加入链表的尾,这时的 node.waitStatus = 0,即最初始化的状态,没有任何意义
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
//重点方法,他会初始化队列,详情看2.4解析
enq(node);
//这里返回构造进队列的节点,直接开始到方法2.5
return node;
}
2.4 enq()方法解析
//同步器的队列为null的时候进来初始化,并让它进队
private Node enq(final Node node) {
//自旋,里面有初始化队列的步骤
for (;;) {
Node t = tail;
//我们可以从这里知道的是,如果同步器的队列本身为null,他会创造一个什么都不代表的节点,即哨兵节点作为head。
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
//第二步自旋会回到这里,进行尾部的设置,新的线程节点进入双向队列
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
2.5 acquireQueued()方法详解
final boolean acquireQueued(final Node node, int arg) {
//假设失败是true
boolean failed = true;
try {
boolean interrupted = false;
//自旋获取锁
for (;;) {
//获取该节点的前一个节点,比如 pre->node,这时候的p拿到的就是pre节点
final Node p = node.predecessor();
//尝试去获取锁,如果锁空闲
if (p == head && tryAcquire(arg)) {
setHead(node);
// help GC,这时候把队列刚开始初始化的头结点踢出去,帮助GC,节省内存,换成了下一个节点为头节点,同样也是作为哨兵节点
p.next = null;
failed = false;
return interrupted;
}
//这里会用的LockSupport的park方法,不要着急,我们慢慢看到2.6、2.7对两个方法的解析就知道是什么样的了
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
2.6 shouldParkAfterFailedAcquire()方法解析
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
//看前置节点的等待状态是怎么样的
int ws = pred.waitStatus;
//前置节点设置了等待唤醒,
if (ws == Node.SIGNAL)
//前置节点设置了等待唤醒,所以这个节点可以安全的进入2.7方法来确定可以调用LockSupport的park方法了
return true;
//当ws>0时,可以知道前置节点绝不是在等待唤醒,我们可以跳过这些没用的节点,相当于删除节点了
if (ws > 0) {
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
//这时找到了pred.waitStatus = 0 并且略过了很多节点
pred.next = node;
} else {
//这里ws = 0 或者 ws = -2 或者 -3,我们直接把节点设置成Node.SIGNAL,即-1,等待正在占用锁的那个successor线程调用LockSupport锁的释放,在队列的线程正在等待调度。
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
//返回失败,证明当前调度的线程还不足以安全的进入park环节,即2.7方法
return false;
}
2.7 parkAndCheckInterrupt()方法详解
//来到最底层的一个算法,可以在这里看到,park方法和interrupted的方法是在park方法后使用的一个方法
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
//返回线程是否已经中断,并清除中断状态
return Thread.interrupted();
}
2.8 lock方法小总结
到目前为止,我们已经完全解析完了AQS的lock源码,可以知道AQS的一个情况就是双向队列要么为null,要么就是没啥用的哨兵节点
多线程工具的源码最好的解读方式就是DEBUG用单线程的方式去先抢占锁一段比较长的时间,再调用其他线程去抢锁看他的方法是怎么走的,这是鄙人目前探索到比较好的一种多线程源码方式,相对于理解也比较好。
2.9 unlock之release方法
//因为整个释放过程是独占线程在操作,基本不存在并发问题,所以特别容易理解
public final boolean release(int arg) {
//先去尝试释放锁,里面的大概流程就是 调整AQS的状态state 和 把AQS的独占线程设置为null
if (tryRelease(arg)) {
Node h = head;
//这里没处理的一个情况就是,队列为null,只有一个线程在使用并抢占着这把锁,所以不需要调用LockSupport去唤醒所谓阻塞的线程
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
//返回true,释放成功
return true;
}
//尝试释放失败
return false;
}
2.10 unlock之unparkSuccessor()方法
//这是用来唤醒队列中被阻塞的节点的
private void unparkSuccessor(Node node) {
//传进来的节点是双向队列的头节点head,即node = head;
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
//找到头节点的下一个节点,从lock方法中我们知道,双向队列的头节点必为哨兵节点或者为null
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
//以上两种情况出现,从队列的尾部开始往前遍历,为什么呢?因为lock的时候是从高并发线程节点是从尾部先插入的并指向前面的,等我粘贴一段代码给你看看
/***
这段代码是在自旋的基础上进行的,只要CAS不成功,它就一直自旋
node.prev = t;//这个很重要,因为node.prev的意思就是它从尾部指向了前面的节点
//CAS不一定会成功,但上面这一行代码一定会成功
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
***/
//从时间的发生点来看,加入有线程刚好去到t.next = node的之前,刚进if方法,并没有执行这句话,那么我们可以知道t.next 可能是空的,所以这里采取了尾部往前遍历的方式找到一个最前面的状态waitStatus状态为 <= 0且不为头节点的snode节点来进行唤醒。
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
//唤醒操作,node结构里有个线程,唤醒该线程就可以了
if (s != null)
LockSupport.unpark(s.thread);
}
总结
总的来说,AQS是一个CLH的变种,AQS的特点是双向队列上每个节点都有一个waitStatus状态,获取不到锁的时候,自旋一段时间后阻塞让出时间片(上下文切换需要),等待前驱节点唤醒后驱节点,这就相当于CLH(自旋) + MCS(MCS是在自己的结点的locked域上自旋等待,即自己调用LockSupport.park())了。