手撕AQS源码(2) – 独占锁的释放
前言
我们分析了独占锁的获取操作, 本篇文章我们来看看独占锁的释放。如果前面的锁的获取流程你已经趟过一遍了, 那锁的释放部分就很简单了, 这篇文章我们直接开始看源码.
一、锁的正确使用姿势
Lock lock = new ReentrantLock();
...
lock.lock();
try {
// 更新对象
//捕获异常
} finally {
lock.unlock();
}
在ReentrantLock源码的类注释上有该块代码。一定要在finally里释放锁!!!
思考
在finally里释放锁就一定可以释放掉锁吗?那redis分布式锁为啥这样写会有问题
- 解答: redis的分布式锁的释放是通过finally代码块里释放锁,但是finally块的代码在断电,杀进程等操作下走不到,即锁无法释放,于是redis中就会一直留有那个key,value,即锁没释放,后续线程无法获得锁.于是想到给key加一个过期时间,但随之而来又有问题, 算了,分布式锁的问题在redis中或者单独开篇讲吧,我们把思绪拉回到ReentrantLock中锁的释放来,开工
二、ReentrantLock的锁释放
1. lock.unlock();
代码如下:
public void unlock() {
sync.release(1);
}
点进去发现直接调用的是AQS中的release()
2. release(int arg)
代码如下(示例):
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
2.1 tryRelease(arg)
经过昨天的分析,此时我们很容易知道,这个tryRelease(arg)肯定是在Sync中实现的,那么开工
protected final boolean tryRelease(int releases) {//release为1
//c只有0和>0的情况,因为肯定是加锁后才调用unlock的,即state的值>=1
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
//c==0表示当前state为1,即没有发生重入,直接释放锁,并将返回tryRlease true
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
//如果c>0,说明发生了重入,将state-1后的值set,返回tryRelease false
setState(c);
return free;
}
思考
为什么此处的setExclusiveOwnerThread(null);没有在cas的前提下,即保证单线程呢?
- 因为解锁肯定是单线程执行的啊
2.2 回到release(int arg)
public final boolean release(int arg) {
if (tryRelease(arg)) {
//如果释放锁成功,h表示head
Node h = head;
//h!=null大概率true,即队列不为空
//h.waitStatus != 0?难道不应该是h.waitStatus==-1么?莫急,下面那个方法就是专门看waitStatue的
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
2.3 unparkSuccessor(Node node)
private void unparkSuccessor(Node node) {//此处node表示头结点
//ws表示头结点waitStatus
int ws = node.waitStatus;
//如果head的waitStatus为-1,则cas设置head的waitStatus为0
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
Node s = node.next;//head的下一个节点
//这段代码意思是: 如果head的下一个节点,即等待队列的第一个节点的waitStatus为1,即放弃等待了, 那么就从尾结点开始遍历,找到距离head节点最近的ws<=0的节点
//s==null,有可能是node节点刚加入队尾的情况,
//s.waitStatus > 0是head的下一个节点放弃了等待
//这两种情况都比较少的
if (s == null || s.waitStatus > 0) {
s = null;
//从尾部遍历,直到找到距离head节点最近的ws<=0的节点
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
//大多数会直接走这里,即唤醒head后面等待的node
if (s != null)
LockSupport.unpark(s.thread);
}
思考
- 为什么此处要用CAS解锁呢?不是说好的解锁是单线程的么?cas岂不是浪费资源
有待解答
至此,解锁代码已经过完了,但是当我读到这里后,我却对加锁代码有了更多的疑问,为了便于学习,姑且记在这里,有待以后解答
问题
在这里先把问题抛出来吧,方便阅读,至于为什么会有这些问题,往下看吧
疑问一
finally中的代码什么时候被执行呢?**
疑问二
线程如果被打断过,为什么要再执行打断自己一次呢? 是谁打断的线程呢?
解答:
- 这么写代码的原因是,我们调用lock.lock(),加的是不可中断锁,所以当使用者手动调用interrupt()时,将不能打断该锁,代码体现了这一点,记下被打断过,继续自旋
- 但是如果加锁时调用的是lockInterruptibly(),则加的是可打断锁,通过追源码可知,是通过抛出异常的方式来实现可打断的
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
疑问三
等待的Node会放弃等待,waitStatus为1,那么是什么时候放弃等待的呢?
1. LockSupport.unpark(s.thread)
唤醒了下一个等待的线程后,发生了什么?
加锁阻塞的代码:
if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
interrupted = true;
这段代码发生在,创建好一个新的Node后放在尾结点,然后执行shouldParkAfterFailedAcquire(p, node): 是否应该park线程当获取锁失败后
1.1 shouldParkAfterFailedAcquire(p, node)
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {//pred前驱节点,node当前节点
int ws = pred.waitStatus;
//如果前驱节点的waitStatus是-1了,那么可以安心去执行park了,即前驱节点会在自己出队的时候叫醒这一个节点
if (ws == Node.SIGNAL)
return true;//返回可以park线程
//如果前驱节点的waitStatus>0即为Node.CANCELLED,则说明前驱节点已经取消了等待(由于超时或者中断等原因)
if (ws > 0) {
do {
//一直往前找,直到找到一个前驱节点的waitStatus是-1
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;//然后直接排在等待锁的节点的后面
} else {
//如果前驱节点的waitStatus为-1, 用CAS设置前驱节点的ws为 Node.SIGNAL,给自己定一个闹钟
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;//返回不应该park,不要忘记这段代码是在for(,,)中,返回false会继续下次自旋
}
- 这段代码总结下来就是,要把新建的这个Node节点放到它的前驱节点的waitStatus=-1的后面,返回true表示已经放好了,可以执行park了
1.2 parkAndCheckInterrupt()
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
前面返回true,可以park了,然后代码执行到LockSupport.park(this);阻塞当前线程,到这里都是上一篇文章的知识,那么在上一个Node释放锁,执行了LockSupport.unpark(s.thread)后发生了什么呢?且听我娓娓道来
1.3 LockSupport.unpark(s.thread)后续
当前在park()的线程被唤醒,执行Thread.interrupted(),即返回当前线程的中断状态,
- 如果为true,那么执行将是否中断过的标志位置为true,然后执行下一轮自旋,抢锁
if (shouldParkAfterFailedAcquire(p, node) &&//请看点进shouldParkAfterFailedAcquire()看解析
parkAndCheckInterrupt())
interrupted = true;
- 如果为false, 那么将进行下一轮自旋,即去抢锁,
对于非公平锁来说,抢不到就又执行下方代码,被park住,抢到锁,继续往下执行,
对于公平锁来说,大概率可以抢到锁,然后执行下方代码,设置抢锁失败标志位failed为false
if (shouldParkAfterFailedAcquire(p, node) &&//请看点进shouldParkAfterFailedAcquire()看解析
parkAndCheckInterrupt())
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null;
failed = false;
return interrupted;
}
然后再return之前,执行finally
finally {
if (failed)
cancelAcquire(node);
}
但是failed总是flase啊,因为要想出for(;😉,就要return,但是在return之前设置了failed = false;,那么这段finallydiamante就不会被执行啊,这是一个疑问???
接着看,
- 当acquireQueued(final Node node, int arg)返回时,带回来boolean interrupted 变量, 即线程有没有被打断过,可以理解为返回的线程的打断状态,
- 因为,parkAndCheckInterrupt()代表线程的打断状态,
- 如果为true, 则设置interrupted =true
- 如果为false,则返回的也是false,
所以acquireQueued(final Node node, int arg)返回的就是线程的打断状态,如果被打断过, 则执行
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)){
selfInterrupt();
}
static void selfInterrupt() {
Thread.currentThread().interrupt();
}
可以看出,如果线程被打断过,则执行打断当前线程, 置打断标志位true???这又是为啥呢?? 这又是一个疑问