JUC并发基石之AQS源码解析–独占锁的获取
上一篇文章中,我们分析了独占锁的获取操作, 这篇文章我们来看看独占锁的释放,释放锁的逻辑相对简单,我们来看源码:
public final boolean release(int arg) {
// 由子类来实现具体的逻辑
if (tryRelease(arg)) {
Node h = head;
// 头节点不为空 并且waitStates不为0
if (h != null && h.waitStatus != 0)
// 唤醒后面的节点
unparkSuccessor(h);
return true;
}
return false;
}
独占锁的涉及到两个函数的调用:
1.tryRelease(arg) ,该方法由AQS的子类来实现释放锁的具体逻辑
2.unparkSuccessor(h) ,唤醒后继线程
我们以ReentrantLock为例看看tryRelease(arg)的实现:
tryRelease(arg)
protected final boolean tryRelease(int releases) {
// 首先将当前持有锁的线程个数减1(因为是由sync.release(1)调用的, releases的值为1)
int c = getState() - releases;
// 当前线程不是持有锁的线程,抛异常
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
// c=0说明锁释放了,那么把占用锁的线程设置为空
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
// 设置状态
setState(c);
return free;
}
我们可以看到,释放锁就是对State进行操作,不过因为是可重入锁,所以只有当state=0的时候,才是真正的释放锁。
假如真正释放锁之后,我们来看看唤醒线程的逻辑:
unparkSuccessor()
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
// 如果head节点的ws比0小, CAS操作将它设为0
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
// 1.正常情况下,后继节点不为空,而且waitStatus <= 0
// 即没有取消获取锁,那么就去唤醒后继节点
// 2. 如果后继节点取消获取锁,那么从尾节点开始找起
// 找排在最前面的等待获取锁的节点
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);
}
我们可以看到后继节点不为空,而且waitStatus <= 0,即没有取消获取锁,那么就去唤醒后继节点。
如果后继节点取消获取锁,那么从尾节点开始找,排在最前面的等待获取锁的节点
这里有一个的问题就是, 为什么要从尾节点开始逆向查找, 而不是直接从head节点往后正向查找, 这样不是更快么?
其实是跟入队的操作有关:
private Node addWaiter(Node mode) {
// 首先创建一个Node节点,传入当前的线程以及Node.EXCLUSIVE
Node node = new Node(Thread.currentThread(), mode);
Node pred = tail;
// 如果队列不为空,因为队列是懒加载的,所以可能为空
if (pred != null) {
// 1. 把当前节点的pre节点设置为尾节点
node.prev = pred;
// 2. 然后CAS设置当前节点为尾节点
if (compareAndSetTail(pred, node)) {
// 3. CAS成功就把之前尾节点的next节点设为当前节点
pred.next = node;
return node;
}
}
// 执行到这里, 只有两种情况:
// 1. 队列为空
// 2. 其他线程在当前线程入队的过程中率先入队,导致尾节点的值改变,所以CAS操作失败
enq(node);
return node;
}
因为这个阻塞队列是双向链表,所以入队的节点前进行第一步和第二步操作,把尾节点设为自己的pre节点,并且CAS设置自己为尾节点,设置成功了,再把之前尾节点的next节点设为当前节点。
所以如果我们unparkSuccessor从头开始遍历,如果CAS设置尾节点成功,但是pred.next的值还没有被设置成node,从前往后遍历的话,有可能遍历不到刚加入的尾节点的。
如果从后往前遍历的话,因为尾节点此时已经设置完成,node.prev = pred操作也被执行过了,那么新加的尾节点就可以遍历到了,并且可以通过它一直往前找。
如果找到了还在等待锁的节点,则唤醒它,也就是调用LockSupport.unpark(s.thread)。
其实也就是唤醒我们上一篇说的挂起的线程:
private final boolean parkAndCheckInterrupt() {
// 挂起的线程会在这里被唤醒
LockSupport.park(this);
return Thread.interrupted();
}
唤醒的线程会在acquireQueued()方法里面继续尝试获取锁:
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;
}
// 在这里被唤醒之后,又会在for循环里面继续尝试获取锁
if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
这样,释放锁就和获取锁结合在一起了。
不过这里我们再关注一点,也就是中断。
上一篇我说过:LockSupport挂起线程,等待被唤醒,有两种情况会被唤醒
- 获得锁的线程在释放锁的时候,会调用release()方法,这个方法会调用LockSupport.unpark()唤醒挂起线程;被唤醒后,在acquireQueued()循环尝试获取锁。
- 当前线程被中断了,那么也会唤醒,唤醒后调用Thread.interrupted()方法返回true,同时中断标志位会被清空,不过在外层会把interrupted 设为 true。
我们可以看到,如果线程是被中断唤醒的,那么调用Thread.interrupted()会返回true,同时中断标志位会被清空。因为parkAndCheckInterrupt()返回true,所以interrupted 设置为true。
if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
interrupted = true;
之后如果获取锁,那么acquireQueued()返回的是interrupted,也就是true,我们再回到acquireQueued()方法调用的地方:
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
如果acquireQueued的返回值为true, 我们将执行 selfInterrupt():
static void selfInterrupt() {
Thread.currentThread().interrupt();
}
它其实就是中断了一下线程。
其实做了这么多,都是因为获取锁的时候是不响应中断的!
所以当在LockSupport.park(this)处被唤醒,可能是因为当前线程在等待中被中断了,因此我们通过Thread.interrupted()方法检查了当前线程的中断标志,并将它记录下来,当它抢到锁了,返回acquire方法后,如果发现当前线程曾经被中断过,就再中断自己一次,将这个中断补上。