AQS源码(二)之独占锁的释放

前言

        Java的内置锁(synchronized)在退出临界区之后是会自动释放锁的,但是ReentrantLock这样的显示锁是需要自己显示的释放的,所以在加锁之后一定不要忘记在finally块中进行显示的锁释放:

Lock lock = new ReentrantLock();
lock.lock();
try{
	...
}catch(..){
	..
}finally{
	lock.unlock();
}

        一定记得要在finally块中释放锁!
        一定记得要在finally块中释放锁!
        一定记得要在finally块中释放锁!

ReentrantLock的锁释放

        由于锁的释放操作对于公平锁和非公平锁都是一样的,所以,unlock的逻辑并没有放在FairSyncNonfairSync里面,而是直接定义在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块在这里实际上并没有什么用,它是为响应中断式的抢锁所服务的。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值