开始之前先提一句,JAVA的内置锁在退出临界区之后是会自动释放锁的,但是ReentrantLock这样的显示锁是需要自己显示释放的,所以在加锁之后一定不要忘记在finally快中进行显示的锁释放:
Lock lock = new ReentrantLock();
...
lock.lock();
try {
// 更新对象
//捕获异常
} finally {
lock.unlock();
}
ReentrantLock的锁释放
由于锁的释放操作对于公平锁和非公平锁都是一样的, 所以, unlock
的逻辑并没有放在 FairSync
或 NonfairSync
里面, 而是直接定义在 ReentrantLock
类中:
public void unlock() {
sync.release(1);
}
release
release方法定义在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;
}
可以看出, 相比获取锁的acquire
方法, 释放锁的过程要简单很多, 它只涉及到两个子函数的调用:
- tryRelease(arg)
- unparkSuccessor(h)
tryRelease
多嘴提醒一下, 能执行到释放锁的线程, 一定是已经获取了锁的线程(这不废话嘛!)
另外, 相比获取锁的操作, 这里并没有使用任何CAS操作, 也是因为当前线程已经持有了锁, 所以可以直接安全的操作, 不会产生竞争.
protected final boolean tryRelease(int releases) {
// 首先将当前持有锁的线程个数减1(回溯到调用源头sync.release(1)可知, releases的值为1)
// 这里的操作主要是针对可重入锁的情况下, c可能大于1
int c = getState() - releases;
// 释放锁的线程当前必须是持有锁的线程
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
// 如果c为0了, 说明锁已经完全释放了
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
unparkSuccessor
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
锁成功释放之后, 接下来就是唤醒后继节点了, 这个方法同样定义在AQS中.
值得注意的是, 在成功释放锁之后(tryRelease
返回 true
之后), 唤醒后继节点只是一个 "附加操作", 无论该操作结果怎样, 最后 release
操作都会返回 true
.
接下来我们就看看unparkSuccessor的源码:
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
// 如果head节点的ws比0小, 则直接将它设为0
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
// 通常情况下, 要唤醒的节点就是自己的后继节点
// 如果后继节点存在且也在等待锁, 那就直接唤醒它
// 但是有可能存在 后继节点取消等待锁 的情况
// 此时从尾节点开始向前找起, 直到找到距离head节点最近的ws<=0的节点
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; // 注意! 这里找到了之并有return, 而是继续向前找
}
// 如果找到了还在等待锁的节点,则唤醒它
if (s != null)
LockSupport.unpark(s.thread);
}
在上一篇文章分析 shouldParkAfterFailedAcquire
方法的时候, 我们重点提到了当前节点的前驱节点的 waitStatus
属性, 该属性决定了我们是否要挂起当前线程, 并且我们知道, 如果一个线程被挂起了, 它的前驱节点的 waitStatus
值必然是Node.SIGNAL
.
下面我们仔细分析 unparkSuccessor
函数:
首先, 传入该函数的参数node就是头节点head, 并且条件是
h != null && h.waitStatus != 0
h!=null
我们容易理解, h.waitStatus != 0
是个什么意思呢?
我们可以逆向思考一下:
当一个head节点的waitStatus
为0说明什么呢, 说明这个head节点后面没有在挂起等待中的后继节点了
如果有的话, head的ws就会被后继节点设为Node.SIGNAL
了,自然也就不要执行 unparkSuccessor
操作了.
另外一个有趣的问题是, 当进入unparkSuccessor方法之后,为什么要从尾节点开始逆向查找, 而不是直接从head节点往后正向查找, 这样只要正向找到第一个, 不就可以停止查找了吗?
首先我们要看到,从后从前找是基于一定条件的:
if (s == null || s.waitStatus > 0)
即后继节点不存在,或者后继节点取消了排队。
另外,我们上一篇分析到了尾分叉:
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode); //将当前线程包装成Node
Node pred = tail;
// 如果队列不为空, 则用CAS方式将当前节点设为尾节点
if (pred != null) {
node.prev = pred; //step 1, 设置前驱节点
if (compareAndSetTail(pred, node)) { // step2, 将当前节点设置成新的尾节点
pred.next = node; // step 3, 将前驱节点的next属性指向自己
return node;
}
}
enq(node);
return node;
}
如果你仔细看上面这段代码, 可以发现节点入队不是一个原子操作, 虽然用了compareAndSetTail
操作保证了当前节点被设置成尾节点,但是只能保证,此时step1和step2是执行完成的,有可能在step3还没有来的及执行到的时候,我们的unparkSuccessor方法就开始执行了,此时pred.next的值还没有被设置成node,所以从前往后遍历的话是遍历不到尾节点的,但是因为尾节点此时已经设置完成,node.prev = pred
操作也被执行过了,也就是说,如果从后往前遍历的话,新加的尾节点就可以遍历到了,并且可以通过它一直往前找。
所以总结来说,之所以从后往前遍历是因为,我们是处于多线程并发的条件下的,如果一个节点的next属性为null, 并不能保证它就是尾节点(可能是因为新加的尾节点还没来得及执行pred.next = node
), 但是一个节点如果能入队, 则它的prev属性一定是有值的,所以反向查找一定是最精确的。
最后, 在调用了 LockSupport.unpark(s.thread)
也就是唤醒了线程之后, 会发生什么呢?
当然是回到最初的原点啦, 从哪里跌倒(被挂起)就从哪里站起来(唤醒)呗:
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this); // 喏, 就是在这里被挂起了, 唤醒之后就能继续往下执行了
return Thread.interrupted();
}
那接下来做什么呢?
如果线程从这里唤醒了,它将接着往下执行。
注意,这里有两个线程:
一个是我们这篇讲的线程,它正在释放锁,并调用了LockSupport.unpark(s.thread)
唤醒了另外一个线程;
而这个另外一个线程
,就是我们上一节讲的因为抢锁失败而被阻塞在LockSupport.park(this)
处的线程。
我们再倒回上一篇结束的地方,看看这个被阻塞的线程被唤醒后,会发生什么。从上面的代码可以看出,他将调用 Thread.interrupted()
并返回。
我们知道,Thread.interrupted()
这个函数将返回当前正在执行的线程的中断状态,并清除它。接着,我们再返回到parkAndCheckInterrupt
被调用的地方:
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);
}
}
可见,如果Thread.interrupted()
返回true
,则 parkAndCheckInterrupt()
就返回true, if条件成立,interrupted
状态将设为true
;
如果Thread.interrupted()
返回false
, 则 interrupted
仍为false
。
再接下来我们又回到了for (;;)
死循环的开头,进行新一轮的抢锁。
假设这次我们抢到了,我们将从 return interrupted
处返回,返回到哪里呢? 当然是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)
处被唤醒,我们并不知道是因为什么原因被唤醒,可能是因为别的线程释放了锁,调用了 LockSupport.unpark(s.thread)
,也有可能是因为当前线程在等待中被中断了,因此我们通过Thread.interrupted()
方法检查了当前线程的中断标志,并将它记录下来,在我们最后返回acquire
方法后,如果发现当前线程曾经被中断过,那我们就把当前线程再中断一次。
为什么要这么做呢?
从上面的代码中我们知道,即使线程在等待资源的过程中被中断唤醒,它还是会不依不饶的再抢锁,直到它抢到锁为止。也就是说,它是不响应这个中断的,仅仅是记录下自己被人中断过。
最后,当它抢到锁返回了,如果它发现自己曾经被中断过,它就再中断自己一次,将这个中断补上。
注意,中断对线程来说只是一个建议,一个线程被中断只是其中断状态被设为true
, 线程可以选择忽略这个中断,中断一个线程并不会影响线程的执行。
最后再小小的插一句,事实上在我们从return interrupted;
处返回时并不是直接返回的,因为还有一个finally代码块:
finally {
if (failed)
cancelAcquire(node);
}
它做了一些善后工作,但是条件是failed为true,而从前面的分析中我们知道,要从for(;;)中跳出来,只有一种可能,那就是当前线程已经拿到了锁,因为整个争锁过程我们都是不响应中断的,所以不可能有异常抛出,既然是拿到了锁,failed就一定是true,所以这个finally块在这里实际上并没有什么用,它是为响应中断式的抢锁所服务的,这一点我们以后有机会再讲。