文章目录
前言
Java的内置锁(synchronized)在退出临界区之后是会自动释放锁的,但是ReentrantLock这样的显示锁是需要自己显示的释放的,所以在加锁之后一定不要忘记在finally块中进行显示的锁释放:
Lock lock = new ReentrantLock();
lock.lock();
try{
...
}catch(..){
..
}finally{
lock.unlock();
}
一定记得要在finally
块中释放锁!
一定记得要在finally
块中释放锁!
一定记得要在finally
块中释放锁!
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):该方法由继承AQS的子类实现,为释放锁的具体逻辑。
- unparkSuccessor(h):唤醒后继线程。
tryRelease():
tryRelease()
由ReentrantLock的静态类Sync
实现,相比较获取锁的操作,这里并没有使用任何CAS操作,也是因为当前线程已经持有了锁,所以可以直接安全的操作,不会产生竞争。
protected final boolean tryRelease(int releases) {
// 首先将当前持有锁的线程个数-1,针对可重入锁。
// releases传进来的值为1(回溯到sync.release(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;
}
unparkSuccessor():锁成功释放后,接下来就是唤醒后继节点了,unparkSuccessor()同样定义在AQS中。值得注意的是,在成功释放锁之后,唤醒后继节点只是一个附加操作,无论该操作结果怎样,release
操作都会返回true。
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)
// 找到了后,并没有return,而是继续向前找
s = t;
}
// 如果找到了还在等待锁的节点,则唤醒它。
if (s != null)
LockSupport.unpark(s.thread);
}
在上一篇文章分析shouldParkAfterFailedAcquire()
时,我们重点提到了当前节点的前驱节点的waitStatus
属性,该属性决定了我们是否要挂起当前线程,并且我们知道,如果一个线程被挂起了,它的前驱节点的waitStatus
必然是Node.SIGNAL
在唤醒后继节点的操作中,我们也需要依赖于节点的waitStatus
值。
下面我们仔细分析unparkSuccessor():
首先,传入该函数的参数node就是头节点head,并且条件是
h != null && h.waitStatus != 0
h!=null
确保等待队列不为空,那h.waitStatus!=0
是什么意思呢?
我们不妨逆向思考一下,什么条件下waitStatus==0
呢?从锁的获取到锁的释放过程中,我们给waitStatus
赋值的地方只有一处,那就是获取锁时调用了shouldParkAfterFailedAcquire
函数,将前驱节点的waitStatus
设为Node.SIGNAL
,除此之外,就没有了。
可是是真的没有了吗?当然还有一处,那就是我们调用addWaiter
时,当我们将一个新的节点添加进等待队列或初始化空队列的时候,都会创建节点,而新建的节点的waitStatus
在没有赋值的情况下被初始化为0.
所以当一个head节点的waitStatus
为0时,说明这个head节点后面没有在挂起等待的后继节点了,也就不用唤醒后继节点了。
那为什么要从尾节点开始逆向查找,而不是直接从head节点往后找?这样只要正向找到第一个,不就可以停止查找了吗?
首先我们要看到,从后往前找是基于一定条件的:
if (s == null || s.waitStatus > 0)
即后继节点不存在,或者后继节点取消了排队,这一条件大多数情况下是不满足的,因为虽然后继节点取消排队很正常,但是通过上一篇我们介绍的shouldParkAfterFailedAcquire
方法可知,节点在挂起前,都会给自己找一个waitStatus
为SIGNAL的前驱节点,对于那些CANCELED的节点都会跳过。
所以,这个从后往前找的目的其实是为了照顾刚刚加入到队列中的节点。
总的来说,之所以从后往前遍历是因为,我们处于多线程环境下,如果一个节点的next属性为null,并不能保证它就是尾节点(可能使用因为新加的尾节点还没来得及执行pred.next = node
),但是一个节点如果能入队,则他的prev属性一定是有值的,所以反向查找一定是最精确的。
最后,在调用了LockSupport.unpark(s.thread)
也就是唤醒了线程之后发生了什么呢?
当然是回到最初的原点了,从哪里跌倒(被挂起),就从哪里站起来(唤醒):
还记得ReentrantLock公平锁的获取过程吗?先判断获取锁状态,判断是否大于0,如果等于0,则判断是不是等待队列中的第一个,如果是,则直接获取锁,如果不是,则入队等待,如果锁状态大于0,判断获取锁的线程是不是自己,如果是自己,即重入,如果不是,则入队等待。入队等待后会尝试继续获取锁,如果获取失败,则判断是不是需要将该线程挂起。注意,一个线程的挂起是在这里被挂起的、
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块在这里实际上并没有什么用,它是为响应中断式的抢锁所服务的。