JAVA并发编程之AQS源码解析二

手撕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);
    }
思考
  1. 为什么此处要用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???这又是为啥呢?? 这又是一个疑问

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值