Juc并发编程14——线程计数器CountdownLatch源码剖析(下)

简介: 线程计数器文章目录线程计数器CountdownLatch源码剖析1 使用计数器锁实现任务计数2 await的源码剖析3 countdown源码剖析CountdownLatch源码剖析

接下来看看doAcquireShared是如何进行阻塞的

 private void doAcquireShared(int arg) {
    //向等待队列中添加一个新的共享锁节点
        final Node node = addWaiter(Node.SHARED); 
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) { // 无限循环
               // 获取当前新建节点的前驱节点
                final Node p = node.predecessor(); 
                //如果前驱节点是头节点,说明当前节点是等待队列的队首节点
                if (p == head) {
                  //再次尝试获取共享锁
                    int r = tryAcquireShared(arg);
                    if (r >= 0) { //获取成功
                       //将当前节点设置为头节点,并且继续唤醒后继节点
                        setHeadAndPropagate(node, r); 
                        p.next = null; // help GC
                        if (interrupted)
                            selfInterrupt();
                        failed = false;
                        return;
                    }
                }
                // 和独占锁类似,没有获取到共享锁,挂起线程
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
              // 如果最后还没有获取到,直接cancel
                cancelAcquire(node);
        }
    }

原来阻塞就是挂起线程呀,我们其实早就知道了,只不过验证了下。而且上面的过程和独占锁其实很类似,不过在获取到节点后,不仅将当前线程设置成了头节点,而且还唤醒了后继节点。这就是共享锁的传播性:当前节点被唤醒后,后继节点也会被唤醒。这是因为可能不止一个线程调用了await方法进行等待。


究竟是如何进行的传播与唤醒呢?走进setHeadAndPropagate来一探究竟吧

 private void setHeadAndPropagate(Node node, int propagate) {
        Node h = head; //取出头节点
        setHead(node); //将当前节点设置为头节点
       //doAcquireShared中传参propagate一定大于0   
       //waitStatus初始为0,SIGNAL=-1;       
      // CONDITION = -2,PROPAGATE = -3;
        if (propagate > 0 || h == null || h.waitStatus < 0 ||
            (h = head) == null || h.waitStatus < 0) {
            // 取出当前节点的后继节点
            Node s = node.next;
            //共享模式或者s为空,继续doReleaseShared
            if (s == null || s.isShared())
              //继续唤醒下一个节点
                doReleaseShared(); 
        }
    }

原来就是判断是不是共享模式,是就继续调用doReleaseShared唤醒下一个节点,doReleaseShared后面会讲.

3 countdown源码剖析

了解完await的底层原理,这里我们接下来看下countdown方法的底层原理。看看它的底层调用方法

public void countDown() {
        sync.releaseShared(1);
}

点进releaseShared

    public final boolean releaseShared(int arg) {
      // 尝试释放锁,成功返回true
      // tryReleaseShared在前面讲await源码时讲过
      //只有当锁数量为0时才会释放成功
        if (tryReleaseShared(arg)) {
            // 继续唤醒后续节点
            doReleaseShared();
            return true;
        }
        return false;
    }

原来countdownawait最后都会调用doReleaseShared唤醒其它节点,前文留下的悬念是时候解开了,那就看看doReleaseShared

private void doReleaseShared() {
        for (;;) {
            Node h = head;
            // 如果头节点不为空,而且头、尾节点不相同
            //说明等待队列中存在节点
            if (h != null && h != tail) {
              // 获取头节点的等待状态
                int ws = h.waitStatus;
                // 如果是SIGNAL(表示后继节点被挂起),
                //就将头节点的状态设置为初始值
                if (ws == Node.SIGNAL) {
                    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                        continue; //失败就重来          
                    // 当头节点被唤醒,会唤醒头节点的后继节点
                    unparkSuccessor(h);
                }
                // 如果头节点的等待状态是初始状态0
                // 尝试将其状态设置为PROPAGATE(表示后继节点已经被唤醒)
                //PROPAGATE状态在setHeadAndPropagate中用到
                //可以让唤醒操作向后继节点传播
                else if (ws == 0 &&
                         !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                    continue;         // loop on failed CAS
            }
            if (h == head)         // loop if head changed
                break;
        }
    }

可能看下来觉得还是比较凌乱,没关系,我们可以回过头对本文章的源码与本专栏的AQS源码多读几遍,这里也梳理下,方便大家理解。

  • 共享锁是线程共享的,同一个时刻可能有多个线程拥有共享锁。
  • 如果一个线程刚获取到了共享锁,那么在其之后等待的线程很有可能也能够获取到共享锁,因此需要传播唤醒后继节点
  • 如果一个线程刚刚释放了线程锁,那么无论是共享锁还是独占锁,都需要传播唤醒后继节点。
目录
打赏
0
0
0
0
4
分享
相关文章
|
29天前
|
【源码】【Java并发】【线程池】邀请您从0-1阅读ThreadPoolExecutor源码
当我们创建一个`ThreadPoolExecutor`的时候,你是否会好奇🤔,它到底发生了什么?比如:我传的拒绝策略、线程工厂是啥时候被使用的? 核心线程数是个啥?最大线程数和它又有什么关系?线程池,它是怎么调度,我们传入的线程?...不要着急,小手手点上关注、点赞、收藏。主播马上从源码的角度带你们探索神秘线程池的世界...
101 0
【源码】【Java并发】【线程池】邀请您从0-1阅读ThreadPoolExecutor源码
面试直击:并发编程三要素+线程安全全攻略!
并发编程三要素为原子性、可见性和有序性,确保多线程操作的一致性和安全性。Java 中通过 `synchronized`、`Lock`、`volatile`、原子类和线程安全集合等机制保障线程安全。掌握这些概念和工具,能有效解决并发问题,编写高效稳定的多线程程序。
114 11
Java线程池提交任务流程底层源码与源码解析
【11月更文挑战第30天】嘿,各位技术爱好者们,今天咱们来聊聊Java线程池提交任务的底层源码与源码解析。作为一个资深的Java开发者,我相信你一定对线程池并不陌生。线程池作为并发编程中的一大利器,其重要性不言而喻。今天,我将以对话的方式,带你一步步深入线程池的奥秘,从概述到功能点,再到背景和业务点,最后到底层原理和示例,让你对线程池有一个全新的认识。
107 12
Java多线程并发编程:同步机制与实践应用
本文深入探讨Java多线程中的同步机制,分析了多线程并发带来的数据不一致等问题,详细介绍了`synchronized`关键字、`ReentrantLock`显式锁及`ReentrantReadWriteLock`读写锁的应用,结合代码示例展示了如何有效解决竞态条件,提升程序性能与稳定性。
525 6
Java 多线程并发编程
Java多线程并发编程是指在Java程序中使用多个线程同时执行,以提高程序的运行效率和响应速度。通过合理管理和调度线程,可以充分利用多核处理器资源,实现高效的任务处理。本内容将介绍Java多线程的基础概念、实现方式及常见问题解决方法。
237 1
Python中的并发编程:探索多线程与多进程的奥秘####
本文深入探讨了Python中并发编程的两种主要方式——多线程与多进程,通过对比分析它们的工作原理、适用场景及性能差异,揭示了在不同应用需求下如何合理选择并发模型。文章首先简述了并发编程的基本概念,随后详细阐述了Python中多线程与多进程的实现机制,包括GIL(全局解释器锁)对多线程的影响以及多进程的独立内存空间特性。最后,通过实例演示了如何在Python项目中有效利用多线程和多进程提升程序性能。 ####
|
1月前
|
Linux编程: 在业务线程中注册和处理Linux信号
本文详细介绍了如何在Linux中通过在业务线程中注册和处理信号。我们讨论了信号的基本概念,并通过完整的代码示例展示了在业务线程中注册和处理信号的方法。通过正确地使用信号处理机制,可以提高程序的健壮性和响应能力。希望本文能帮助您更好地理解和应用Linux信号处理,提高开发效率和代码质量。
57 17
|
1月前
|
Linux编程: 在业务线程中注册和处理Linux信号
通过本文,您可以了解如何在业务线程中注册和处理Linux信号。正确处理信号可以提高程序的健壮性和稳定性。希望这些内容能帮助您更好地理解和应用Linux信号处理机制。
63 26
|
3月前
|
Java多线程编程秘籍:各种方案一网打尽,不要错过!
Java 中实现多线程的方式主要有四种:继承 Thread 类、实现 Runnable 接口、实现 Callable 接口和使用线程池。每种方式各有优缺点,适用于不同的场景。继承 Thread 类最简单,实现 Runnable 接口更灵活,Callable 接口支持返回结果,线程池则便于管理和复用线程。实际应用中可根据需求选择合适的方式。此外,还介绍了多线程相关的常见面试问题及答案,涵盖线程概念、线程安全、线程池等知识点。
323 2
Java多线程编程的陷阱与解决方案####
本文深入探讨了Java多线程编程中常见的问题及其解决策略。通过分析竞态条件、死锁、活锁等典型场景,并结合代码示例和实用技巧,帮助开发者有效避免这些陷阱,提升并发程序的稳定性和性能。 ####

热门文章

最新文章