1 state常量
AQS是实现各种锁的基础,提供对state(资源)的获取和阻塞等待,阻塞的线程会放入一个队列中,AQS通过一个链表来实现该队列,我们后面来看Node链表。通俗一点来说,就是维护一个资源(state),谁要使用,就去获取这个资源,获取不到就进队列等待。也就是锁咯。
其中对state的操作均为原子操作:
//volatile关键字修饰
private volatile int state;
//只读取,为原子操作
protected final int getState() {
return state;
}
//只设置值,为原子操作
protected final void setState(int newState) {
state = newState;
}
//改变值,通过CAS来保证原子操作
protected final boolean compareAndSetState(int expect, int update) {
// See below for intrinsics setup to support this
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
2 Node对象
锁非为共享锁和排他锁,AQS对锁的获取分为两类:SHARED(共享锁)和EXCLUSIVE(排他锁)。我们看下Node的源码:
static final Node SHARED = new Node();
static final Node EXCLUSIVE = null;
有两种锁就有两种实现方式:
- 排他锁需要实现
tryAcquire(int):独占方式。尝试获取资源,成功返回true,失败返回false。
tryRelease(int):独占方式。尝试释放资源,成功返回true,失败返回false。 - 共享锁需要实现
tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
tryReleaseShared(int):共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false。
Node对象中等待状态取值:
默认值为0,表示新加入的节点。负值表示结点处于有效等待状态,而正值表示结点已被取消。
//表示当前结点已取消调度。当timeout或被中断(响应中断的情况下)会触发变更为此状态,进入该状态后的结点将不会再变化。
static final int CANCELLED = 1;
//表示后继结点在等待当前结点唤醒。后继结点入队时,会将前继结点的状态更新为SIGNAL。
static final int SIGNAL = -1;
//表示结点等待在Condition上,当其他线程调用了Condition的signal()方法后,CONDITION状态的结点将从等待队列转移到同步队列中,等待获取同步锁。
static final int CONDITION = -2;
//共享模式下,前继结点不仅会唤醒其后继结点,同时也可能会唤醒后继的后继结点。
static final int PROPAGATE = -3;
3 排他锁lock的过程
能不能获取到锁取决于AQS子类自己对state状态的判断,AQS维护的是同步队列,将获取不到锁的线程对象维护到一个队列中进行管理。先理解每个方法的含义,看不懂就多看几遍。然后我们分析如何操作入队列和出队列的。
3.1 acquire方法
AQS不实现具体的尝试获取锁的逻辑,获取锁将由其子类去实现。排他锁获取锁失败,就会将当前线程通过addWaiter方法加入到等待队列中,为什么会执行打断线程方法,我们后面看(只是设置中断标志为true,请见线程状态)。
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
static void selfInterrupt() {
Thread.currentThread().interrupt();
}
3.2 addWaiter方法
加入等待队列,addWaiter(Node.EXCLUSIVE),这里的Node.EXCLUSIVE标志这是一个排他锁。源码如下
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}
- new Node(Thread.currentThread(), mode),执行Node的构造方法,其中第一个参数为当前获取锁的线程,第二个参数为**排他锁标志(Node.EXCLUSIVE)**创建一个节点:
Node(Thread thread, Node mode) { // Used by addWaiter
this.nextWaiter = mode;
this.thread = thread;
}
节点信息如图:
![在这里插入图片描述](https://img-blog.csdnimg.cn/b4b1a1a0d9184f529dd21fe0c4ed8125.png
- Node pred = tail;因为要将当前新创建的节点node加入到队列中,就像你去排队一样(队伍有人),先去寻找队尾那个人,他就相当于你的前一个人,你就相当于他的后一个人,所以这里把尾节点取出来赋值给pred,表示当前node的前置节点
- if (pred != null)相当于tail !=null 说明队列已经存在,node加入队列就行,注意这里CAS设置尾节点如果失败进入后续的enq方法,如果CAS设置尾节点成功就表示成功将node加入到了队列中,并且返回当前node,也相当于当前队列的尾节点。
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
3.3 enq方法
如果队列为空就要依赖当前方法初始化队列,如果队列不为空就会一直循环,直到把node加入到队列中为止。其中tail和head均被volitile修饰,cas操作保证,在并发情况下不会构建多个队列,也不会出现多个节点指向同一个前驱节点。
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
和addWaiter方法中入队列的逻辑基本相同,只是这里入CAS设置node为尾节点失败了会一直循环,我们看一下如果队列为空,初始化后,再将当前node加入队列,队列是怎样的。
初始化的队列如图:
最外框是抽象出来的链表对象,表示队列,head和tail为该队列的头和尾,前后节点相互关联,这样就形成了一个完整的队列
将node节点加入到队列中后入下图:
到此就相当于把获取不到锁的线程成功加入到队列中了,那是不是只要加入队列中就完事了呢?并不是的,此时当前线程还并没有阻塞等待,我们接着看acquireQueued方法。
3.4 acquireQueued方法
前面的步骤已经将当前线程成功的加入到了队列中,我们看接下来AQS的操作,还是先上源码:
这里参数node,由前面我们知道是当前线程的node,arg不是AQS关注的重点,是子类获取锁逻辑的参数。
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 {
if (failed)
cancelAcquire(node);
}
}
if (p == head && tryAcquire(arg))获取当前node的前置节点,如果是头节点就再尝试获取一次锁,获取到了说明排队排到了,把前置节点踢出队列(都排到我了,你前面的人还在队里肯定没有意义,所以移出去)。这里的逻辑比较简单。
3.5 shouldParkAfterFailedAcquire方法
前置节点不是头节点,或者尝试获取锁失败,是不是就要阻塞当前线程呢?还不是的。
- if (ws == Node.SIGNAL)如果前置节点的状态为SIGNAL,说明前置节点都再等待被唤醒,那我肯定也要进入等待状态了
- if (ws > 0) 如果前置状态不是再等待即状态不为SIGNAL,再判断是不是已经取消了,如果取消了,就将取消的节点移出队列
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.6 parkAndCheckInterrupt方法
- **LockSupport.park(this)**中断当前node节点所在的线程
- **Thread.interrupted()**当被唤醒后,返回当前线程的打断状态,并重置为false。
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
Thread.interrupted():为什么要获取线程的中断状态,并将其重置为false呢?
从上面的过程来看好像有点多此一举了,但是考虑一种情况,当前线程被唤醒去获取锁,此时又获取锁失败,是不是又得重新执行park方法,让它继续阻塞而不能for循环,消耗CPU。在park方法源码中:若是发现当前线程中断状态为true,则直接返回不再挂起线程。若是调用Thread.isInterrupted(),中断状态没有改为false,那么当调用LockSupport.park()方法时,线程是无法挂起的。
3.7 cancelAcquire方法
取消获取尝试获取锁,当acquire中出现异常后会进入到当前方法中。该方法得本质就是将当前异常得节点从队列移除出去。移出去的同时要考虑需不要唤醒后继节点,那为什么要考虑这个呢?你试想一下,如果刚好有别的线程唤醒你,你正常执行完要去唤醒你后面的节点,但是现在你异常了,你不能不管后面的节点了呀,唤醒动作要传播下去,不然整个队列的线程都会阻塞了。
private void cancelAcquire(Node node) {
if (node == null)
return;
node.thread = null;
Node pred = node.prev;
// 如果当前节点得前置节点有状态为1得,就都从队列中移除
while (pred.waitStatus > 0)
node.prev = pred = pred.prev;
// 获取前置节点得next节点
Node predNext = pred.next;
// 将当前节点状态设置成1
node.waitStatus = Node.CANCELLED;
//如果当前节点是尾节点,那就将他得前置节点设置为尾节点
if (node == tail && compareAndSetTail(node, pred)) {
// 新得尾节点得,下个节点肯定为null咯
compareAndSetNext(pred, predNext, null);
} else {
int ws;
// 如果pred不是头节点,说明他在队列中间
// 如果它得状态不是1,或者可以设置成-1,并且他得线程还不能为空
if (pred != head &&
((ws = pred.waitStatus) == Node.SIGNAL ||
(ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
pred.thread != null) {
// 取出当前节点得next节点
Node next = node.next;
// 如果next节点存在且状态不是取消,那就将它得前置节点得next节点
if (next != null && next.waitStatus <= 0)
compareAndSetNext(pred, predNext, next);
} else {
// pred为头节点,或者状态不为-1,或者其thread为null,都需要唤醒后继节点
unparkSuccessor(node);
}
node.next = node; // help GC
}
}
- **if (node == tail && compareAndSetTail(node, pred))**判断node是不是尾节点,并且能将前置节点设置为尾节点,如果设置失败,说明node后面此时又加入了新的节点了,就不是尾节点了。
- if (pred != head &&
((ws = pred.waitStatus) == Node.SIGNAL ||
(ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
pred.thread != null) {
乍一看,这个判断好复杂哦,难以理解,其实在判断三种情况,首先判断前置节点是不是头节点,不是的话,再判断前置节点的状态是不是正常的(这里的正常指的是前置节点状态是或者能被设置为SIGNAL),最后判断节点线程是不是null,注意最后一个判断网上很多说是判断初始化的空节点(因为初始化的空节点其thread是null),但是这里肯定不是,空节点肯定是头节点,这里明显不是头节点,所以这种说法是错误的,这里的情况是判断前置节点刚好也异常了,进入了cancelAcquire方法执行了node.thread = null;但是还没有执行node.waitStatus = Node.CANCELLED;。总结就是node前面有个正常等待被唤醒节点。 - **if (next != null && next.waitStatus <= 0)**执行到这里说明队列前面有正常排队的,那我后面如果有正常排队的,需要将我前后的节点关联起来。
3.8 unparkSuccessor方法
解锁一个后继节点。
private void unparkSuccessor(Node node) {
// 获取当前节点的状态,如果状态小于0,就将当前节点的状态修改成0
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
// 获取当前节点的后继节点
Node s = node.next;
// 如果后继节点为空或者后继节点状态为被取消,则需要遍历队列
if (s == null || s.waitStatus > 0) {
s = null;
// 从队列的尾部开始遍历,依次向前取,直到前置节点为null或者为当前节点结束
// 这里注意,虽然是倒着遍历,但是取得是正向第一个不为null并且状态小于0得那个节点
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
//如果有符合条件的节点对象,则唤醒该节点关联的线程
if (s != null)
LockSupport.unpark(s.thread);
}
虽然从尾部遍历,但是最终取得是node1,而不是node3
为什么要倒着遍历取正向第一个不为null得且状态小于0得值呢?直接正向遍历不更快嘛?
如图所示要取得是node1节点
- 当前节点得后继节点为null时,无法通过正向遍历找到后继节点,但是个人觉得一般应该不会出现这种情况,node得后继节点为null应该只会出现在node恰好是尾节点得情况。
- 另一个更重要的原因是因为入队列的方法,compareAndSetTail(pred, node)当此方法返回成功,尾节点的值已变更,但是这时尾节点的前一个节点的next不一定赋值了,逻辑可以看一下前面的enq方法逻辑的图。所以如果正向遍历的话,通过node.next取值,可能会丢失尾节点。
4 排他锁unlock过程
当前节点执行完了,我得通知队列中后续节点取获取锁呀。在AQS中执行以下方法通知后续队列获取锁。
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
- **if (tryRelease(arg))**如何释放锁,对state的操作由子类实现,如果释放锁成功就要通知队列中的后续节点取获取锁。
- if (h != null && h.waitStatus != 0)头节点不为空且头节点等待被唤醒,那就执行唤醒操作unparkSuccessor方法。
5 排他锁队列
什么时候入队列,什么时候出队列呢?经过上面的代码分析,我们来总结一下入队列和出队列的情况。
5.1 入队列
获取不到锁的线程会执行addWatier方法放入队列中,节点状态刚入进去的时候是0,在shouldParkAfterFailedAcquire方法中将其设置为-1(SIGNAL),注意设置的是前面一个节点而不是当前节点。。排他锁的队列只用到了三种状态:分别是0初始化,-1(SIGNAL)等待被唤醒,1(CANCELLED)异常被取消。
5.2 初始化的空节点出队列
当前有个A线程第一次获取到了锁,A线程没有入队列并一直持有锁没释放,
- 此时node线程进来了,获取不到锁,就被阻塞住了
- 当A线程执行完毕,会通过release方法中的**if (h != null && h.waitStatus != 0)去判断是不是需要唤醒后继线程,如下队列发现需要唤醒,那就执行unparkSuccessor(head)**方法。
- unparkSuccessor方法会先将head状态设置成0,然后将head的next节点唤醒,此时node线程从阻塞中唤醒。
- 此时node线程再次执行acquireQueued方法中的循环,**if (p == head && tryAcquire(arg))**正好node的前置节点是头节点,并且成功获取到了锁,那就将头节点从队列中剔除出去了。node节点此时变成头节点了。
综上所述,释放锁后的唤醒操作始终是唤醒头节点的后继节点,而不是头节点。头节点是初始化的空节点,或者是已经运行完成并释放了锁的节点。
头节点从队列断开逻辑入下图。
5.3 完成运行的节点出队列
继续上面的逻辑,node节点释放锁之前有新的线程来争抢锁,那后来的线程就会继续入到队列里面,后续node释放锁后,后来的节点获取锁成功就会像5.2的逻辑一样,把node节点也踢出队列。
5.4 节点异常出队列
在acquireQueued方法中有异常处理,异常后都会执行cancelAcquire方法,当异常后需要将当前节点从队列中移除出去,移除的同时需要考虑,是不是刚好唤醒当前异常的节点去获取锁了,如果这样的话需要处理唤醒逻辑,不然唤醒就无法传递下去,整体队列就永远阻塞了。
5.4.1 刚好是尾节点异常
尾节点异常,就只需要将尾节点从队列中移除就行,首先要保证CAS能成功操作node5成为尾节点,如果不成功的话,说明有其它线程CAS操作设置了新的尾节点,此时就变成了node6异常节点就变成了中间节点异常的情况了。我们这里只考虑CAS设置尾节点成功。最后将node5的后置节点设置为null。
**compareAndSetNext(pred, predNext, null)**这个也有可能失败,但是失败没关系,说明有新的节点入到队列中来了。
if (node == tail && compareAndSetTail(node, pred)) {
compareAndSetNext(pred, predNext, null);
这里有个疑问点,此时node6的前置节点还是还是node5,个人理解:从垃圾回收的角度来说,node6应该没有被任何对象引用了,是根不可达的,可以被垃圾回收。
5.4.2 异常节点的前置正常节点是头节点
node2异常了,执行到循环判断前置节点状态是不是也异常时,此时刚好node0和node1节点也异常了,并且已经设置了状态为1 node.waitStatus = Node.CANCELLED;。此时刚好node2节点的前置节点指向了头节点。此时进入unparkSuccessor(node2)方法中。
唤醒node3节点后,最后将node2的next节点指向自己,帮助垃圾回收。
此时会唤醒node3节点,node3节点进入acquireQueued方法中尝试获取锁:
-由于此时node3的前置节点还是node2,此时进入**shouldParkAfterFailedAcquire(node2,node3)**方法中。前node3前面的异常节点都移除,node3前置变成node,node的后继变成node3。
- 能获取到锁,说明node线程释放了锁,并且调用unparkSuccessor(node)方法,发现第一个后继节点异常,从尾节点开始向前遍历,最终再次唤醒node3。
最终node3获取到了锁,就会setHead(node3),该方法内会将node3前驱节点设置成null;队列就会变成如下状态:
private void setHead(Node node) {
head = node;
node.thread = null;
node.prev = null;
}
至此异常的节点从队列中移除。
node0和node1也会执行上述步骤,最终如下:这里有个疑问,那就是异常得节点前置节点都没有置空,梳理了很多次逻辑都是这样得结果,希望有大佬指点迷津。
5.4.3 异常节点是中间节点
- 在cancelAcquire方法中,队列得前置节点在正常等待锁得时候,异常需要将自己移出队列。这个方法执行完,并没有完全得移除。
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)
compareAndSetNext(pred, predNext, next);
}
- 在node2完成执行,调用unparkSuccessor(node4)唤醒后。node4再执行acquireQueued方法。
此时node3异常节点才从队列中真正的踢出去。
5.4.4 总结一下出队列
最终出队列都是在acquireQueued方法中获取到锁后,把当前节点的前置节点置空,并且将之前把当前节点当作后置节点的节点的后置节点也置空,此时才把处理完成和它前面异常的节点一并踢出队列。
unparkSuccessor方法只是唤醒后面正常等待被唤醒的节点,不会改变队列。
shouldParkAfterFailedAcquire是将当前节点的前驱节点指向前置正常的节点,会改变队列的。
cancelAcquire是将当前节点的前驱节点指向前置正常的节点,会改变队列的。