【Java并发】-- ReentrantLock 可重入锁实现原理2 - 释放锁

接着上一篇分析……
给一扇传送门,【Java并发】-- ReentrantLock 可重入锁实现原理1 - 获取锁
当ThreadA线程执行完任务后调用finally中的unlock()方法释放锁的时候会经历什么样的操作。

ReentrantLock.unlock()


1. ReentrantLock中的unlock()

	/*
	* 释放锁
	*/
    public void unlock() {
        sync.release(1);  // sync是继承了AQS的静态内部类
    }

2. AQS中的release()

public final boolean release(int arg) {
    if (tryRelease(arg)) {  // 释放锁成功
        Node h = head; 		// 获取到AQS队列中的头结点
        if (h != null && h.waitStatus != 0) // 如果 头节点不为空 并且状态!=0.
        //调用 unparkSuccessor(h)唤醒后续节点
            unparkSuccessor(h);
        return true;
    }
    return false;
}

这里面有两个核心方法,这么机智的你肯定一眼就看出来了,就是tryRelease()和unparkSuccessor();

3. AQS 中的tryRelease(),

嗯哼,这块和tryAquire()是一样的,都是模板方法模式的体现,都没有给出具体的实现,真正的tryRelease() 实现还是需要在子类,也就是ReentrantLock的内部类sync中重写tryRelease();

    protected boolean tryRelease(int arg) {
        throw new UnsupportedOperationException();
    }

4. ReentrantLock中的tryRelease()

这个方法可以认为是一个设置锁状态的操作,通过将state状态减掉传入的参数值(参数为1),结果状态为0,就将独占锁的owner设置为null,方便其他线程有机会抢占锁;

  • 在独占锁加锁的时候,我们会将state状态+1,不清楚的看上一篇获取锁的过程,里面有分析到;
  • 所以,同一个锁,在重入多次之后,可能被叠加多次,比如重入了4次,最后state的值就是4,getState()得到的结果就是4;
  • 在进行unlock的时候,就需要减掉所有的重入次数,才能完全释放锁;
  • 也就是unlock()的次数与lock()次数对应上,才能将owner设置为null,释放掉锁,最后返回true;
       /*
       * 释放锁
       */       
        protected final boolean tryRelease(int releases) {
            int c = getState() - releases;   //getState获去到总重入次数减去1
            if (Thread.currentThread() != getExclusiveOwnerThread()) // 判断当前线程是否是获取到锁的线程,如果不是就抛异常 (只有获取到锁的当前线程才能释放锁!!)
                throw new IllegalMonitorStateException();
            boolean free = false;
            if (c == 0) {  // 最后减到为0表示释放完毕
                free = true;
                setExclusiveOwnerThread(null);
            }
            setState(c); // 更新重入次数,直到为0 ,否则return false,继续走释放的逻辑
            return free;
        }

5. AQS的unparkSuccessor()

直到tryRelease()返回true,完全释放锁成功后,我们会调用AQS的unparkSuccessor()唤醒后继节点,英文注释都是大师Doug Lea写的,为了不曲解他的本意,放着让大家看看;

    /**
     * Wakes up node's successor, if one exists.
     *
     * @param node the node
     */
    private void unparkSuccessor(Node node) {
        /*
         * If status is negative (i.e., possibly needing signal) try
         * to clear in anticipation of signalling.  It is OK if this
         * fails or if status is changed by waiting thread.
         */
        int ws = node.waitStatus; // 获得 head 节点的状态
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0); // 设置 head 节点状态为 0

        /*
         * Thread to unpark is held in successor, which is normally
         * just the next node.  But if cancelled or apparently null,
         * traverse backwards from tail to find the actual
         * non-cancelled successor.
         */
        Node s = node.next; //得到 head 节点的下一个节点
        if (s == null || s.waitStatus > 0) {
        //如果下一个节点为 null 或者 status>0 表示 cancelled 状态.
        //通过从尾部节点开始扫描,找到距离 head 最近的一个waitStatus<=0 的节点
            s = null;
            for (Node t = tail; t != null && t != node; t = t.prev)
                if (t.waitStatus <= 0)
                    s = t;
        }
        if (s != null)
         //next 节点不为空,直接唤醒这个线程即可
            LockSupport.unpark(s.thread);
    }

为什么这里是从tail扫描找距离head最近的节点,从head开始扫描的不是更近更快吗?
上一篇分析的enq()构建节点方法里面,最后一步是 t.next=node,设置原tail的next节点指向新的节点,若在 cas 操作之后, t.next=node 操作之前。 存在其他线程调用 unlock 方法从 head开始往后遍历,由于 t.next=node 还没执行意味着链表的关系还没有建立完整。就会导致遍历到 t 节点的时候被中断,因为tail还没有指向新的尾结点。 所以若从后往前遍历,一定不会存在这个问题。

6. 挂起的线程继续执行

脑补一下上篇文章画过的图,现在ThreadA已经释放完锁,也唤醒了后置节点ThreadB,所以ThreadB要继续执行,那它会从哪开始执行呢?
思考一下ThreadB被阻塞到哪了?
是的,被阻塞在lock()中调用的AQS.acquireQueued()中了。
下面我们关注一下ThreadB 被唤醒以后的执行流程;

  1. 把 ThreadB 节点当成新的 head
  2. 把原 head 节点的 next 节点指向为 null,断开ThreadA
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值