ReentrantLock流程图示及队列节点唤醒剔除解析

目录

简介 

ReentrantLock加锁流程图示

ReentrantLock解锁流程图示

队列中node数据的流转

addWaiter

acquireQueued的shouldParkAfterFailedAcquire方法

release

扩展cancelAcquire方法


简介 

ReentrantLock是一个可重入且独占式的锁,它具有与使用synchronized监视器锁相同的基本行为和语义,但与synchronized关键字相比,它更灵活、更强大,增加了轮询、超时、中断等高级功能。ReentrantLock,顾名思义,它是支持可重入锁的锁,是一种递归无阻塞的同步机制。除此之外,该锁还支持获取锁时的公平和非公平选择。

synchronized与ReentrantLock的区别:https://segmentfault.com/a/1190000039091031

ReentrantLock加锁流程图示

ReentrantLock解锁流程图示

 下面根据源码,探究队列中数据的整体流转:

队列中node数据的流转

addWaiter

通过添加到队列的源码中可以看到,tail不为空时,CAS将当前节点的prev设置为tail节点,然后将之前tail的next指向新tail的节点。若tail为空或者CAS失败,enq兜底,以死循环的方式,若tail为空,说明不存在队列数据,CAS设置空NODE到head、tail中,然后CAS将当前node添加至tail节点,直到CAS成功设置到tail节点后返回(此时并没有判断之前tail的状态)。

/**
     * 为当前线程和给定模式创建和排队节点(队列中添加独占node节点,线程、node类型)
     */
    private Node addWaiter(Node mode) {
        Node node = new Node(Thread.currentThread(), mode);
        // Try the fast path of enq; backup to full enq on failure
        Node pred = tail;
        /**
         * 当队列中尾节点不为null,队列中的尾节点设置为当前新增node的头节点,队列的尾节点设置当前新增队列
         * 当CAS设置尾节点失败时,通过enq操作兜底。(此时未理会tail节点的状态)
         */
        if (pred != null) {
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        // CAS抢占队列尾节点失败或者不存在尾节点时
        enq(node);
        return node;
    }

 private Node enq(final Node node) {
        // 死循环
        for (;;) {
            // 尾节点若为空,CAS设置新节点到head中(初始化队列)
            Node t = tail;
            if (t == null) { // Must initialize
                if (compareAndSetHead(new Node()))
                    // 首尾node都是新初始化的node节点
                    tail = head;
            } else {
                // 若队列已存在nocde,将尾节点设置为当前node 的头节点进行CAS
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    // 成功后,再将队列之前的尾节点的next指向新尾节点
                    // (由此可以看出,是先建立node尾节点指向之前队列节点,后续建立队列节点next指向新node节点)
                    t.next = node;
                    return t;
                }
            }
        }
    }

acquireQueued的shouldParkAfterFailedAcquire方法

当不是头节点或者获取锁失败时,设置node的前置节点的状态,当prev节点的状态>0,也就是取消状态时,node的prev属性会根据之前的prev节点数据一直往前找,直到找到非取消状态的node节点,然后,进行关联。这样当node插入队列时,即梳理去除掉了部分取消状态的node节点。

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL)
            /*
             * This node has already set status asking a release
             * to signal it, so it can safely park.
             * 这个节点已经设置状态要求释放信号,当前节点处于休眠状态,LockSupport.park()
             */
            return true;
        if (ws > 0) {
            /*
             * Predecessor was cancelled. Skip over predecessors and
             * indicate retry.
             * 队列中的线程被取消了,跳过找下一个,直到线程状态不是取消状态(从后往前找)
             * 发现传入的前驱的状态大于0,即CANCELLED。说明前驱节点已经因为超时或响应了中断,
             * 而取消了自己。所以需要跨越掉这些CANCELLED节点,直到找到一个<=0的节点
             * (node:新加的尾节点,若prev的状态是取消,则继续往前找prev,直到找到未取消的node)
             */
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            /*
             * waitStatus must be 0 or PROPAGATE.  Indicate that we
             * need a signal, but don't park yet.  Caller will need to
             * retry to make sure it cannot acquire before parking.
             * node设置为可park的状态,在part之前,会再次发起获取锁的请求。
             */
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

但是node节点的状态是随时可能变更为取消状态的,很可能是node关联后,前置节点改为取消状态。这里先看下解锁时唤醒节点的流程。

release

当head节点不为空,且状态不为0时,说明h的next节点已经存在。通过unparkSuccessor去唤醒队列中的第一个节点。

情况一:若头节点的next为空,说明node刚刚CAS建立了prev节点,头节点还未来得及设置next,则从tail往前获取到头部第一个非取消状态的node信息,进行唤醒。

情况二:若头节点状态为取消状态,说明next节点已取消,则遍历队列从后往前得到头部第一个非取消状态的node信息,进行唤醒。

public final boolean release(int arg) {
        if (tryRelease(arg)) {
            /**
             * 获取等待队列中头节点,若头节点不为空,且等待状态不为0时,执行unparkSuccessor
             * (即:存在等待队列且头节点的状态不是正常状态)
             */
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

/**
     * 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.
         * 若节点状态小于0,CAS等待状态为0,可抢锁状态
         */
        int ws = node.waitStatus;
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 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.
         * unpark的线程保存在后续节点中,通常是下一个节点。但如果取消或明显为空,*从尾部向后遍历,以找到实际的*未取消的后续。
         */
        // 若节点next为空、若为取消状态,那么从tail进行队列的唤醒
        Node s = node.next;
        if (s == null || s.waitStatus > 0) {
            s = null;
            // 从后往前遍历,获取得到非取消状态的node,进行唤醒
            for (Node t = tail; t != null && t != node; t = t.prev)
                if (t.waitStatus <= 0)
                    s = t;
        }
        if (s != null)
            LockSupport.unpark(s.thread);
    }

我们知道,当唤醒线程后,线程仍然会执行死循环,判断是否是头节点、CASstatus值成功后,标志着获取到锁返回。若头节点的状态为cancel,从后往前获取得到的非取消状态的node的prev并不是头节点,岂不是没有tryAcquire获取锁的资格,也就始终存在在队列中?

当进行第一次循环时,确实无法得到有效执行,当进入shouldParkAfterFailedAcquire()时,会判断队列中第一个获取状态的node节点的prev数据是否是取消状态,依次往前找到第一个非取消状态的节点,然后再次循环争抢锁。

注:因为node取消时,是改变自身node状态,依次,遍历到头节点时,便可以有效退出当前循环,执行获取锁的流程。

扩展cancelAcquire方法

线程因为中断、超时等,未成功获取到锁,流转到cancelAcquire方法。

node的线程设置为null,且根据node的prev往前找到第一个非取消状态的node节点(有效节点),将当前node置为取消状态,注:这里可以看到,改变的是node本身的状态之前,而不是前置节点的状态,所以当从后往前遍历校验状态值时,头节点一定不是取消状态。

情况一:当node为尾节点时,CAS设置prev有效节点为tail节点,若成功,CAS将新的tail节点的next置为空,置空结果无需理会,失败了说明存在其他线程竞争设置到tail节点了,属于正常流程。

情况二:当node即不是尾节点,又不是头节点时,prev节点设置为休眠时,将node节点的next节点替换到prev节点的next中。

情况三:当node是头节点的next节点时,唤醒node

/**
     * Cancels an ongoing attempt to acquire.
     * 取消正在进行等待的node节点
     * @param node the node
     */
    private void cancelAcquire(Node node) {
        // Ignore if node doesn't exist
        if (node == null)
            return;

        node.thread = null;

        // Skip cancelled predecessors
        // node的节点前置的node状态校验,若已取消,循环设置并判断之前node状态,直到找到非取消状态的node节点
        Node pred = node.prev;
        while (pred.waitStatus > 0)
            node.prev = pred = pred.prev;

        // predNext is the apparent node to unsplice. CASes below will
        // fail if not, in which case, we lost race vs another cancel
        // or signal, so no further action is necessary.
        // 前置节点的next指向
        Node predNext = pred.next;

        // Can use unconditional write instead of CAS here.
        // After this atomic step, other Nodes can skip past us.
        // Before, we are free of interference from other threads.
        // 当前node设置为取消状态
        node.waitStatus = Node.CANCELLED;

        // If we are the tail, remove ourselves.
        // 若是尾节点,将前置节点的nextCAS设置为空(尾节点取消,将非取消状态的最近的前置节点CAS为尾节点)
        if (node == tail && compareAndSetTail(node, pred)) {
            // 失败,说明有其他线程加入到队列中,tail就应该是最新加入的线程
            compareAndSetNext(pred, predNext, null);
        } else {
            // 非尾节点或者CAS尾节点失败
            // If successor needs signal, try to set pred's next-link
            // so it will get one. Otherwise wake it up to propagate.
            int ws;
            // 当前置节点不为头节点且状态为休眠,线程不为空时,将当前node的后置nodeCAS设置到pred节点上去
            if (pred != head &&
                ((ws = pred.waitStatus) == Node.SIGNAL ||
                 (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
                pred.thread != null) {
                Node next = node.next;
                if (next != null && next.waitStatus <= 0)
                    compareAndSetNext(pred, predNext, next);
            } else {
                // 头节点,唤醒后续节点,因当前节点为1,所以会过滤唤醒,执行后续节点
                unparkSuccessor(node);
            }

            node.next = node; // help GC
        }
    }

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
ClickHouse 是一个分布式列式数据库管理系统,可以处理大规模数据集并支持高吞吐量的并行查询。要实现往多节点本地表轮询写,可以按照以下步骤进行: 1. 配置多个 ClickHouse 节点:首先,需要在每个节点上安装和配置 ClickHouse,确保所有节点都是运行正常的。 2. 创建本地表:在每个节点上创建相同的本地表结构。可以使用相同的表名和列定义,确保表结构的一致性。 3. 使用负载均衡器:可以使用负载均衡器来进行写入请求的轮询分发。负载均衡器可以将写入请求均匀地分发到每个节点上,以实现数据的平衡写入。 4. 编写应用程序逻辑:在应用程序中,可以使用 ClickHouse 的客户端库进行连接到负载均衡器,并通过执行 INSERT 语句将数据写入到多个节点的本地表中。在每次写入数据时,可以通过循环或其他方式来选择下一个节点进行写入,以实现轮询写入的目的。 5. 数据复制和同步:在多节点本地表轮询写入的情况下,需要注意数据的复制和同步问题。可以使用 ClickHouse 的分布式表功能来实现数据的自动复制和同步,以保证数据的一致性和可靠性。 总结起来,要实现往多节点本地表轮询写入,需要配置多个 ClickHouse 节点,创建相同的本地表,使用负载均衡器进行请求的分发,并编写应用程序的逻辑来实现数据的轮询写入。同时,还需要关注数据的复制和同步,以确保数据的一致性和可靠性。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

旺仔丷

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值