AQS
全称:AbstractQueuedSynchronizer,抽象的队列同步器。
作为Java中的锁的祖宗类,在并发编程中占有不可撼动的地位,像JUC里的ReentrantLock类、CountDownLatch类,ReentrantReadWriteLock类、Semaphore类都是将AQS作为基类来继承的,包括我们经常使用的Lock,底层也是用AQS的思想来实现。
这篇文章以ReentrantLock为例聊一下AQS的底层实现原理,在聊之前,先说明一下AQS的出现是为了解决什么问题?
为何出现
在高并发场景下,共享资源被某个线程占用,那么就需要一定的阻塞等待唤醒机制来保证锁的分配,AQS里主要是通过CLH队列的变体来实现的,为什么是变体,原本的CLH队列是单向链表,而AQS里的CLH队列增加了指向前一个元素的指针,使这个队列变成了双向链表,这个队列的作用主要是存放暂时获取不到锁的线程,这个队列就是AQS同步队列的抽象表现,它将要请求共享资源的线程及自身的等待状态封装成队列里的节点对象Node,通过CAS、自旋和LockSupport的park方法,维护state变量的状态,使并发达到同步状态。
案例结合源码分析
接下来通过一个场景案例来理解一下AQS:
现在有A、B、C三个客户去办理业务,当A开始办理业务时,业务大厅的屏幕上会显示窗口状态变量state从原本的0(没有人)变为-1(有人正在办理业务),这个时候,B和C就会进入等候区(CLH队列)
这个图并不严谨,现在仅作了解,现在结合源码进行分析
A线程执行,lock加锁
首先一开始先创建一把非公平锁,A线程获得锁,A线程进行业务办理
public static void main(String[] args) {
//非公平锁
ReentrantLock reentrantLock = new ReentrantLock();
ReentrantLock reentrantLock1 = new ReentrantLock(true);
new Thread(()->{
reentrantLock.lock();
try {
try {
TimeUnit.MINUTES.sleep(5);
} catch (InterruptedException e) {throw new RuntimeException(e);}
} finally {
reentrantLock.unlock();
}
},"A").start();
}
我们看到,通过lock方法A线程得到一把锁,进入lock方法看发生了什么
final void lock() {
//CAS算法,修改state值为1
if (compareAndSetState(0, 1))
//设置独占锁给A线程
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
进入方法,首先是一个CAS算法,A线程期望当前state值为0也就是空闲,如果是空闲就把state值变为1,并给当前线程A一把独占锁。
公平锁和非公平锁的区别
非公平锁和公平锁的区别就在于这一块代码,公平锁的lock方法是在等待前方线程执行完毕之后,根据CLH队列里的顺序让下一个空闲线程继续执行,也就是排队执行,很有秩序,而非公平锁则是如果前方线程执行完毕,则CLH队列里所有的线程全部来争抢这一把锁,哪怕这个线程在队列的最前边,也不一定会拿到锁,这就是公平锁与非公平锁的根本区别。
由于A执行业务需要5分钟,紧接着B线程开始执行
ReentrantLock reentrantLock2 = new ReentrantLock(true);
new Thread(()->{
reentrantLock.lock();
try {
try {
TimeUnit.MINUTES.sleep(5);
} catch (InterruptedException e) {throw new RuntimeException(e);}
} finally {
reentrantLock.unlock();
}
},"B").start();
进入lock方法
final void lock() {
//state=1 与预期值0不符,修改失败
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
//尝试修改状态失败,进入acquire方法
acquire(1);
}
首先CAS,希望state=0,如果是则把state值改为1,可是当前state值被A设置为了1,所以if不走,直接else,进入acquire方法。
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
AQS三核心
tryAcquire--尝试获取锁
acquir方法是AQS的核心,里面的判断首先进入第一个tryAcquire方法,结合图代码分析
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
final boolean nonfairTryAcquire(int acquires) {
//获取当前引用线程=B
final Thread current = Thread.currentThread();
//获取当前state值=1
int c = getState();
//c=1
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
//B是不是获得锁的线程,不是,往下走
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
//返回false
return false;
}
addWaiter--进入队列
CLH队列是一个双向链表,里边包含着一个同步器,同步器有两个属性,head前指针属性指向队列中第一个Node对象,tail尾指针属性指向队列中最后一个Node对象
图解:举例
private Node addWaiter(Node mode) {
//新建一个Node对象 将B线程进行封装
Node node = new Node(Thread.currentThread(), mode);
//没有对象进入tail为空,所以队列同步器中前指针pred=null
Node pred = tail;
//不走
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
//新建的node进入队列
enq(node);
//返回node
return node;
}
跟上脚步,进入enq
private Node enq(final Node node) { //B线程的node节点
//进入循环
for (;;) {
// t=null
Node t = tail;
//进入判断
if (t == null) { // Must initialize必须初始化
new一个空的Node对象设置为CLH队列里的Head头节点--虚拟节点
if (compareAndSetHead(new Node()))
tail = head; //将head头节点Node对象赋值给tail尾节点
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
代码第一次循环执行完,此时的CLH队列
第一次循环--虚拟节点
第一次进入addWriter方法时,会创建一个虚拟节点,也叫做哨兵节点,用于占位,不明白的可以看一下上图代码分析
代码进入第二次循环
private Node enq(final Node node) { //B线程的node节点
//第二次进入循环
for (;;) {
// 虚拟节点赋值给t
Node t = tail;
//此时t不等于null
if (t == null) {
if (compareAndSetHead(new Node()))
tail = head;
} else {
//将虚拟节点赋值给B节点的前指针
node.prev = t;
//CAS算法--将同步器的尾节点t指向B节点
if (compareAndSetTail(t, node)) {
//虚拟节点的next设置为B节点
t.next = node;
//返回t,完成入队操作
return t;
}
}
}
}
代码执行完,队列如下:
此时B进入队列,好巧不巧,这个时候C开始执行
ReentrantLock reentrantLock2 = new ReentrantLock(true);
new Thread(()->{
reentrantLock.lock();
try {
try {
TimeUnit.MINUTES.sleep(5);
} catch (InterruptedException e) {throw new RuntimeException(e);}
} finally {
reentrantLock.unlock();
}
},"C").start();
C线程进入lock
final void lock() {
//尝试更改状态,此时state=1,更改失败
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
//进入acquire
acquire(1);
}
跟进
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
首先C线程进入tryAcquire方法
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
final boolean nonfairTryAcquire(int acquires) {
//获取当前线程C
final Thread current = Thread.currentThread();
//获取当前state值,此时A还办理业务,所以state=1
int c = getState();
//不走
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
//C线程是拿到锁的线程吗?否,不走
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
//返回false
return false;
}
C线程来到addWaiter方法
private Node addWaiter(Node mode) {
//new一个Node节点,封装C线程,后续称C节点
Node node = new Node(Thread.currentThread(), mode);
// 尾节点赋值给前置指针
Node pred = tail;
//判断成立进入
if (pred != null) {
//C节点的前置指针指向尾节点B
node.prev = pred;
//设置CLH队列中同步器的尾指针指向C节点
if (compareAndSetTail(pred, node)) {
//前一个节点B的next指向C节点
pred.next = node;
返回C节点
return node;
}
}
enq(node);
return node;
}
C成功入队,此时队列就变成下图
acquireQueued--阻塞+唤醒
假设按顺序B开始执行acquireQueued方法,提前说明一下,下边比较复杂,想看的建议有个心理准备,不耐心看不明白,结合上图队列图来看
//node=B节点 arg=state=1 A占用 =0时空闲
final boolean acquireQueued(final Node node, int arg) {
//定义一个布尔变量
boolean failed = true;
try {
boolean interrupted = false; //含义:是否中断业务,默认不中断
//正常进入循环
for (;;) {
// 第一次和第二次循环 :p = B的前置节点,也就是虚拟节点--哨兵节点
final Node p = node.predecessor();
/* 第一次和第二次循环:判断这个虚拟节点是不是队列中头节点,并且判断
这里B线程是否抢占锁成功,这里是false,A未执行完没有释放锁 */
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
/* 第一次循环:这个方法是状态的改变也是一个布尔的返回,这里是false,
详情看下图代码,不进入内部继续回到第二次循环 */
/* 第二次循环:第一个方法是状态的改变也是一个布尔的返回,这里是true,
详情看下边代码,第二个方法里有LockSupport.park()方法
底层C实现追不到,在这里就可以理解为B就卡在这里了,一直转圈,等待后续
唤醒 */
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
//两次循环--不要看混了
//方法入参 pred=前置节点-头节点,也就是虚拟节点 node是B节点
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
/* 获取头结点的waitStates值,ws=0,队列里的节点初始化时在队列中的状态
waitStates默认值都是0 */
int ws = pred.waitStatus;
//判断ws是不是-1,-1说明是SIGNAL状态
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算法,把pred的状态值waitStates改为-1 —— 第一次循环更改的,
第二次循环刚进入就返回true */
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
//返回false
return false;
}
shouldParkAfterFailedAcquire方法会让当前线程把前一个节点的waitStates值都变为-1,而且因为LockSupport.park()方法,在A线程没有释放锁之前所有进入的线程都会在这里阻塞着,老老实实的在队列里呆着。
这里B开始转圈等待锁的释放的同时C也开始执行acquireQueued方法了,同理,A没有执行完和B一样转圈卡住,等待锁的释放 ,这里B和C在锁释放前就在这里被阻塞了,也可以理解为被挂起了这里acquireQueued方法还未执行完。
A线程unlock释放锁,准备唤醒
public static void main(String[] args) {
//非公平锁
ReentrantLock reentrantLock = new ReentrantLock();
ReentrantLock reentrantLock1 = new ReentrantLock(true);
new Thread(()->{
reentrantLock.lock();
try {
try {
TimeUnit.MINUTES.sleep(5);
} catch (InterruptedException e) {throw new RuntimeException(e);}
} finally {
//业务办理完毕,准备释放锁
reentrantLock.unlock();
}
},"A").start();
}
进入unlock方法
//方法1
public void unlock() { // 进入方法2
sync.release(1);
}
//方法2
public final boolean release(int arg) {
// 进入方法3,获取返回结果
if (tryRelease(arg)) {
Node h = head;
//头节点不等于null并且头结点的waitstatus不等于0
if (h != null && h.waitStatus != 0)
//进入方法4
unparkSuccessor(h);
return true;
}
return false;
}
//方法3
protected final boolean tryRelease(int releases) {
// c = 当前State值-1 = 0
int c = getState() - releases;
//如果当前线程不等于持有锁的线程那就报异常
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
//定义布尔变量 默认值false
boolean free = false;
//c=0 进入
if (c == 0) {
free = true;
//锁的占有线程设置为null
setExclusiveOwnerThread(null);
}
//将state状态值设置为0,表明其余线程可以抢锁了
setState(c);
//返回方法2
return free;
}
//方法4
private void unparkSuccessor(Node node) { // 传入头节点
// ws = -1
int ws = node.waitStatus;
//-1<0 进入
if (ws < 0)
//CAS->重新将头节点的waitStatus设置为0
compareAndSetWaitStatus(node, ws, 0);
//将头节点的next也就是B节点赋值给s
Node s = node.next;
// s!=null 、s的waitStatus=-1 不进入判断
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);
}
此时unlock走完,唤醒B线程,进入到B线程阻塞的地方
//方法1:
final boolean acquireQueued(final Node node, int arg) {
//定义一个布尔变量
boolean failed = true;
try {
boolean interrupted = false; //含义:是否中断业务,默认不中断
for (;;) {
//B节点被唤醒了,B节点的前一个节点赋值给p,也就是虚拟节点=p
final Node p = node.predecessor();
// p=头节点,且B要尝试抢占锁,进入方法4获取返回值true,进入if
if (p == head && tryAcquire(arg)) {
//进入方法6->头节点设置为B节点
setHead(node);
//设置虚拟节点的next为null
p.next = null; // help GC 虚拟节点被垃圾回收掉
failed = false;
//返回false,至此出队完成
return interrupted;
}
//进入方法3返回false,继续循环
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
//方法2
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
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;
}
//方法3
private final boolean parkAndCheckInterrupt() {
//取消阻塞
LockSupport.park(this);
//返回false
return Thread.interrupted();
}
//方法4
protected final boolean tryAcquire(int acquires) {
//进入方法5获取返回值->获取到true返回到方法1
return nonfairTryAcquire(acquires);
}
//方法5
final boolean nonfairTryAcquire(int acquires) {
//获取当前线程B
final Thread current = Thread.currentThread();
//获取state值,由于当前A线程锁释放,所以c=0
int c = getState();
//进入
if (c == 0) {
//B线程将state设置为1
if (compareAndSetState(0, acquires)) {
//B线程抢到独占锁
setExclusiveOwnerThread(current);
//返回true到方法4
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;
}
//方法6
private void setHead(Node node) {
//头节点设置为B节点
head = node;
//B节点的thread设置为null
node.thread = null;
//B节点的前置指针设置为null
node.prev = null;
//走完回到方法1
}
上述代码执行完,开始new的虚拟节点被垃圾回收掉了,B线程抢到锁,B节点成为了新的虚拟节点,也就是哨兵节点,这个时候队列如下图:
线程离开
以上是顺利的情况下的执行流程,那么如果中间又线程不想等待了,退出了,又该怎么办呢?
这里以C线程退出为例
回到之前代码进行分析
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
//如果有线程退出了,这里举例中间的C线程退出
if (failed)
//进入方法二
cancelAcquire(node);
}
}
//方法2
private void cancelAcquire(Node node) { //传入C节点
//C节点!=null
if (node == null)
return;
//将C节点的thread设置为null
node.thread = null;
//将C节点的prev也就是B节点赋值给pred
Node pred = node.prev;
//B节点的waitStatus值=-1,不进入循环
while (pred.waitStatus > 0)
node.prev = pred = pred.prev;
//将B节点的next也就是C节点赋值给predNext
Node predNext = pred.next;
//将C节点的waitStatus改为1 出队
node.waitStatus = Node.CANCELLED;
//第一个判断,C节点是否是尾节点,显然不是,直接走else
if (node == tail && compareAndSetTail(node, pred)) {
compareAndSetNext(pred, predNext, null);
} else {
//定义ws
int ws;
//这里是true 直接进入 感兴趣的读一下
if (pred != head &&
((ws = pred.waitStatus) == Node.SIGNAL ||
(ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
pred.thread != null) {
//C的nextD节点赋值给next next=D节点
Node next = node.next;
//D节点!=null并且D节点的waitStatus=0,进入if
if (next != null && next.waitStatus <= 0)
//CAS->将原来B节点的next设置为D节点
compareAndSetNext(pred, predNext, next);
} else {
unparkSuccessor(node);
}
//C节点的next指向自己
node.next = node; // help GC
}
}
C离开后队列变成这样
至此,关于AQS基本分析完毕,本来以为很简单,但是逐行分析确实令人暴躁,建议大家在心平气和的时候阅读这篇文章,码字不易,留下一个赞呗!!!