这几天刚好在看并发相关内容,谈到并发就离不开锁,那我们就来聊聊AQS吧。 AQS是AbstractQueuedSynchronizer的简称,顾名思义,抽象队列式同步器,FIFO结构的一个队列
先讲明白AQS相关的概念:
1.AQS的概念?上面说过了,他是AbstractQueuedSynchronizer的简称,是由Doug Lea完成编写的(完成JUC的大佬,java.util.concurrent包,据说他的每一行代码都是经典,确实如此)。
2.AQS帮我们完成了什么事?他能让我们专注于资源的获取和释放的逻辑,他本身则构建了一个双向队列来实现线程对资源获取的排队工作。好了,这是他的作用,后面我们慢慢细聊。
3.AQS有几种模式?独占式(只能有一个线程获取锁)以及共享式(可以有多个线程获取锁然后并发执行,只有资源数足够)
4.他的实现有哪些?像Mutex,ReentrantLock(独占锁),ReentrantReadWriteLock(共享锁)等等,都是基于AQS来实现的,ReentrantLock我们比较常用,他是可重入(获取锁的线程可再次获取)的排它锁(只能有一个获取锁)
5.他和Synchronized帮我们实现的锁有什么区别?一个是自己实现的一个是JVM帮我们实现的,功能嘛非常明显,自己定制实现的功能肯定多一些啦。但是JVM是一个monitorenter和monitorexit自动获取和释放锁,而基于AQS实现的就需要我们手动获取和释放了。
源码分析
说了上面几个点,也看不出来AQS是个啥东东,我们还是慢慢上代码来细细体会,学习得静下心来,方能事半功倍。
字段
private transient volatile Node head;
private transient volatile Node tail;
private volatile int state;
AQS很重要的就是这三个字段了,他分别是AQS维护的队列的头和尾。而这个int类型的state则是资源,提供了三个方法来对state进行修改。可以注意到他们都是使用了volatile关键字进行修饰的,所以是保证了线程的可见性,A的修改,其他线程都能感知到,并重新从内存中进行读取。
protected final int getState()
protected final void setState(int newState)
protected final boolean compareAndSetState(int expect, int update)//使用cas对state进行修改
里面还有一个Node类,他是AQS维护的队列中的节点
static final class Node{
static final Node SHARED = new Node();//独占式锁节点
/** Marker to indicate a node is waiting in exclusive mode */
static final Node EXCLUSIVE = null;//共享式锁节点
static final int CANCELLED = 1;
static final int SIGNAL = -1;
static final int CONDITION = -2;
static final int PROPAGATE = -3;
}
里面有几个值先混个眼熟,后续还会讲到
| 名称 | 具体含义 |
|---|---|
| CANCELLED | 表示当前节点已经取消被AQS调度的机会,中断和timeout情况下会触发此状态 |
| SIGNAL | 表示后继结点在等待自己将他唤醒。后继结点入队时,会将前继结点的状态更新为SIGNAL |
| CONDITION | 当前节点现在在Condition等待队列上,当有别的线程调用了Condition.signal方法后,他会从等待队列移动到同步队列,等待被AQS调度获取锁执行 |
| PROPAGATE | 这个是在共享模式下的状态,前驱节点会在执行足够的情况下唤醒后面的n的节点,直到资源不足 |
| DEFALUT | 初始状态,隐式赋值,表示新节点入队时的状态 |
然后下面就开始介绍独占锁和共享锁的具体实现
独占式锁实现原理
获取资源
AQS提供了一个顶层的入口框架
//这是独占锁模式的顶层入口
public final void acquire(int arg) {
/**tryAcquire是获取资源,成功返回true,否则false
***addWaiter是将线程加入到队列尾部
***acquireQueued是让线程阻塞在队列中,并且自旋获取资源,获取到了才会返回,
返回值true是中断过,false是没有中断,
*/
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
- tryAcquire:尝试获取资源state。
- addWaiter:如果没有获取到资源,则将线程封装为一个独占式Node,放入同步队列尾部,并将tail设置为这个节点。
- acquireQueued:放入同步队列后自旋检查自己的前驱节点是不是head了,即当前位置是第二个Node,那么表示自己可以去执行自己的事了(FIFO先进先出结构,AQS总是调度等待时间最长的,即最先入队的),如果不是第二,那就乖乖的LockSupport.park()去吧(阻塞)。
tryAcquire
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
怎么抛异常啊?之前说过AQS只是管理获取资源线程的入队维护工作,而获取资源和释放资源是用户按照自己逻辑定义的。
addWaiter
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;
if (pred != null) {//尝试快速添加,因为enq会自旋,可能比这个慢
node.prev = pred;
//使用cas的将tail尾结点设为当前的node节点
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);
return node;//返回新添加的node节点
}
如果快速添加失败,这个enq是干啥的啊?来看一下
private Node enq(final Node node) {
for (;;) {//注意这里是自旋,一定会将node节点给放在尾结点
Node t = tail;
if (t == null) { // Must initialize,如果tail为空也就是当前队列没有节点,会创建一个哨兵节点,然后继续构建链表
if (compareAndSetHead(new Node()))
tail = head;//只是创建了个哨兵节点,自旋后走下面的else
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {//cas的将尾结点设置成当前node节点
t.next = node;
return t;
}
}
}
}
这里挺妙的,自旋的将node添加进尾部。代码也确实十分优美,这就是Dong Lea吗?不愧是大哥李。
acquireQueued
好了这里我们已经将获取不了资源的节点加入到尾部节点了,接下来怎么办啊?得不到就睡觉去呗,该躺平了,就像我们去银行取钱,工作台只有一个,我们拿了个号(排队号addWaiter),然后就可以去沙发上等着前一个人叫我们(哈哈哈,虽然现实中他可能直接溜了)。
final boolean acquireQueued(final Node node, int arg) {//返回是否中断过
boolean failed = true;
try {
boolean interrupted = false;//检测是否被中断过
for (;;) {//自旋
final Node p = node.predecessor();//获得node的前驱节点
//当排到老二的位置了,就能去尝试获取资源了,又是我们自定义逻辑
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
//确定自己不是老二,应该寻找一个安全点(自旋的寻找)然后park了。
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);//取消资源获取,将node的状态设为cacell
}
那这个shouldParkAfterFailedAcquire干了啥呢?寻找一个安全点
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;//node的前驱节点
if (ws == Node.SIGNAL)//"pre记得叫我,我是node",好了现在可以安心的park阻塞了
return true;
if (ws > 0) {//若前面的节点是cacelled,就需要寻找安全点,然后被跳过的这些cacell节点会被gc
/*
* Predecessor was cancelled. Skip over predecessors and
* indicate retry.
*/
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {//-3,-2,0,叫他们提醒自己(把前驱设成signal通知自己)
/*
* 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.
*/
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
//调用它的acquireQueued会自旋的进行设置
return false;
}
下面就是park睡觉啦,如果是在同步队列中线程调用了interrupt方法,他会记录,但是不进行响应(啥也不干)。
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
在最外层调用的acquire,条件都成立,他会执行这个方法,他仅仅是将线程的中断标志设置为true(因为在waiting时调用了interrupt方法会清除中断标志)
static void selfInterrupt() {
Thread.currentThread().interrupt();
}
释放资源release
释放资源简单一些,就是将node移除
这里release同样是释放资源的顶层入口
public final boolean release(int arg) {
//尝试释放资源
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
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)
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.
*/
Node s = node.next;
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)
LockSupport.unpark(s.thread);//唤醒下一个符合条件的节点
}
好了,我们开始有点小疑问了,为什么node你唤醒你的下一个节点是从tail尾巴找过来的,而不是从node.next开始找啊?这不都是一样的吗?答案是不一样的,这个跟之前的代码有联系。 我们找到acquireQueued这个方法,可以看到
final boolean acquireQueued(final Node node, int arg) {//返回是否中断过
try {
for (;;) {
final Node p = node.predecessor()
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC,1号位置
failed = false;
return interrupted;
}
...省略
}
} finally {
if (failed)
cancelAcquire(node);
}
private void cancelAcquire(Node node) {
...省略
node.waitStatus = Node.CANCELLED;
// If we are the tail, remove ourselves.
if (node == tail && compareAndSetTail(node, pred)) {
compareAndSetNext(pred, predNext, null);
} else {
...省略
node.next = node; // help GC,2号位置
}
为啥从后找,就跟1,2号位置有关啦
1号位置是头节点,p.next=null,毋庸置疑help gc,2号位置node.next = node?这为啥help gc,他是将自己断开,成为一个孤立节点,但是你可能会问,他的pre还在啊。是的,确实还在,但是并不影响GC,逻辑上已经可以回收了,就像你1->2->3->4你想删除2后面的节点,你只需要2.next=null即可。现在如果从前往后找,那么到这个节点的时候next链是断开的,我找不着下一个了,所以你需要从后往前寻找安全一些,画了个图示。
这是一种可能的情况,并不会每次都发生。但是从后往前会安全一些。
总结
现在独占锁大概讲明白了,获取不到资源addWaiter然后acquireQueued(自旋)。释放资源unparkSuccesor寻找下一个符合条件的Node。每次只能选择一个Node。
共享锁实现原理
有了前面独占锁的知识,学习共享锁快的多。
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
获取共享资源
doAcquireShared
这个是共享锁的顶层入口,可以看到这个tryAcquireShared也是我们自己控制,但是语义规定好了,>=0表示资源足够,<0表示资源不足,需要入队
private void doAcquireShared(int arg) {
final Node node = addWaiter(Node.SHARED);//进入等待队列,节点类型为共享式
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {//自旋
final Node p = node.predecessor();//前驱节点
if (p == head) {//当前node是老二
int r = tryAcquireShared(arg);//尝试获取一下资源
if (r >= 0) {//资源足够,线程可以做自己的事情的去了,这个老二可以return了
setHeadAndPropagate(node, r);//这里不太一样,除了设置新的head,还会在资源足够情况下释放相邻节点。a->b->c。
p.next = null; // help GC
if (interrupted)//处理一下中断
selfInterrupt();
failed = false;
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);//取消等待
}
}
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head; // Record old head for check below
setHead(node);
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
Node s = node.next;//刚才拿到资源的是node,现在node变成head了,接着拿下一个老二
if (s == null || s.isShared())
doReleaseShared();
}
}
可以发现,好像跟acquireQueued差不多啊,只是说中断处理,在函数内处理了,而acquireQueued是在外部处理的。然后就是当某个节点处于老二位置苏醒了,还会唤醒相邻节点,而独占锁是拿到资源的节点release后才来唤醒,共享锁是拿到资源的节点就能直接唤了。确实学习的挺快的,哈哈哈。
释放共享资源
releaseShared
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {//自己重写,释放资源的逻辑
doReleaseShared();
return true;
}
return false;
}
private void doReleaseShared() {
/*
* Ensure that a release propagates, even if there are other
* in-progress acquires/releases. This proceeds in the usual
* way of trying to unparkSuccessor of head if it needs
* signal. But if it does not, status is set to PROPAGATE to
* ensure that upon release, propagation continues.
* Additionally, we must loop in case a new node is added
* while we are doing this. Also, unlike other uses of
* unparkSuccessor, we need to know if CAS to reset status
* fails, if so rechecking.
*/
for (;;) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
unparkSuccessor(h);//唤醒下一个节点
}
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
if (h == head) // loop if head changed
break;
}
}
总结
好了,差不多AQS原理也讲完了,这个类里面有2000多行代码,还有一些其他的方法,比如支持响应中断的acquireInterrupt,跟上面的acquire其实差不多。然后现在来总结一下大致的流程。
- 独占锁:当一个线程申请不到资源时,他就会将直接封装为一个Node节点然后调用addWaiter方法,放入同步队列中,返回返回刚在加入的这个node,再调用acquireQueued方法,不断的进行自旋(如果不是老二他会park),直到他的前一个node,release释放了资源后才会来唤醒当前这个node。
- 共享锁:同样的,他申请不到资源时(调用tryAcquireShared方法返回值<=0),也会进行资源等待(doAcquireShared方法),如果当前节点被前一个节点唤醒,他会尝试获取资源,一旦能获取成功他就能执行Thread自己的事情,并且将当前node设为Head,并唤醒相邻的节点。