ReentrantLock源码分析(二)

        在上一篇 并发编程之ReentrantLock源码分析_leon7199的博客-CSDN博客中,介绍了ReentrantLock的使用方法、基本原理和源码分析。看完上文之后,不知大家是否有什么疑问,我对源码有以下两个疑问

问题1:如何判断队列中是否有节点(线程)在排队

问题2:锁释放流程,unparkSuccessor方法中for循环为什么是从尾节点(tail)开始

我们先看一下问题1,判断队列是否有节点(线程)排队的源码:

public final boolean hasQueuedPredecessors() {
        Node t = tail; 
        Node h = head;
        Node s;
        return h != t &&
            ((s = h.next) == null || s.thread != Thread.currentThread());
    }

我们可以看到,满足 h != t &&((s = h.next) == null || s.thread != Thread.currentThread()) 表达式,表示队列有节点在排队,我们把表达式拆分一下

条件1: h != t,这个比较好理解,表示头节点 和尾节点指向不同的节点,说明队列不为空

条件2: (s = h.next) == null,将头节点的next节点赋值给临时节点s。头节点的next节点什么情况为null呢,是不是很疑惑,我们暂时先跳过,下面会详细讲解

条件3: s.thread != Thread.currentThread(),这个比较好理解,下一个节点的线程不等于当前线程,表示队列有其他线程

因此,只有条件1成立 并且条件2 或 条件3 成立)时,表示队列中有节点排队。那么在什么情况下,条件1和条件2同时成立,也就是头节点和尾节点不相等,并且头节点的下一个节点为null呢?我们先回顾一下节点加入队列的源码吧,答案就在其中:

private Node addWaiter(Node mode) {
        Node node = new Node(Thread.currentThread(), mode);
        Node pred = tail;
        if (pred != null) {
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        //入队
        enq(node);
        return node;
    } 

private Node enq(final Node node) {
        for (;;) {
            Node t = tail;
            if (t == null) { // Must initialize
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

在第一篇介绍ReentrantLock文章的基础上,我们可以看到,当第一个排队的节点调用addWaiter方法加入队列时,执行第一步,首先会通过new Node()创建一个空节点,并且headtail都指向空节点,如下图所示                       

        当执行到第二步,线程t1执行到enq()方法中 t.next = node 语句时,让出cpu,给其他线程执行;此时head指向Node0,tail指向Node1,Node1的prev指向Node0,还没来得及将之前的尾节点Node0的next节点指向Node1时,让出了cpu。

head=Node0,tail=Node1
Node1.prev=Node0, Node0.next=null

此时,条件1  h != t 成立,并且条件2 (s = h.next) == null 成立。

总结:在并发情况下,会出现 (s = h.next) == null 的情况。

脑袋突然闪过一句话what the  fxxk,源码的作者DougLea到底是怎么样想到这种情况的,测试都不一定能复现出这种情况,作者却能想到,思维太严谨了。

  我们继续看问题2,源码如下:

public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            //如果头节点不为空,并且waitStatus != 0
            if (h != null && h.waitStatus != 0)
                //调用unparkSuccessor(h)唤醒后续节点
                unparkSuccessor(h);
            return true;
        }
        return false;
    }


private void unparkSuccessor(Node node) {
        int ws = node.waitStatus;
        if (ws < 0)
             // 设置 head 节点状态为 0
            compareAndSetWaitStatus(node, 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)
                    s = t;
        }
        if (s != null)
            LockSupport.unpark(s.thread);
    }

可以看到,unparkSuccessor()方法的入参是头节点head。将head的next节点赋值给临时节点s。

s.waitStatus >0比较好理解,表示此节点为cancelled状态,而队列中可能有多个节点已经为cancelled状态,所以都要过滤掉,因此要从尾节点tail进行遍历,找到距离head最近的一个waitStatus<0的节点

当head的next节点s为空时,流程不是可以结束了吗,为什么还要从尾节点遍历,寻找要唤醒的节点。

其实,这和我们问题1产生的场景一样,当并发情况下,head.next==null,而队列中是有线程在排队的,head节点和tail节点指向如下所示

head=Node0,tail=Node1
Node1.prev=Node0, Node0.next=null

所以当( s == null || s.waitStatus > 0)时,要从尾节点tail进行遍历,找到距离head最近的一个waitStatus<0的节点唤醒。

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值