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

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

1 使用计数器锁实现任务计数

多任务同步神器,它允许一个或多个线程,等待其它线程完成工作,比如我们现在有一个需求:

  • 有20个任务,需要将每个任务的执行结果算出来,但是每个任务执行的时间未知。
  • 当所有的任务执行结束后,立即整合统计所有的执行结果。

我们并不知道任务可以在什么时间完成,因此执行统计的时间不好设置,设置短了则还有任务没有完成,设置长了则统计延迟。

CountdownLatch可以做到,它是一个实现子任务同步的工具。Demo如下

  public static void main(String[] args) throws InterruptedException {
        CountDownLatch latch = new CountDownLatch(20);
        for (int i = 0; i < 20; i++) {
            final int  finalI = i;
            new Thread(() ->{
                try {
                    Thread.sleep((long)(2000 + new Random().nextDouble()));
                    System.out.println("thread " + finalI + " finished");
                } catch (InterruptedException exception) {
                    exception.printStackTrace();
                }
                latch.countDown(); // 相当于计数器,每次减少1
            }).start();
        }
        latch.await(); //可以多个线程同时等待,这里仅演示了一个线程进行等待
        System.out.println("all sub task finished!");
    }

执行结果如下

其实它就是一个线程计数器,注意CountDownLatch是一次性的,不能重复使用。比如下面再多调用一次latch.await,程序还是正常结束的(毕竟计数不可逆,已经是0了,而且无法将计数器重置).


 public static void main(String[] args) throws InterruptedException {
        CountDownLatch latch = new CountDownLatch(20);
        for (int i = 0; i < 20; i++) {
            final int  finalI = i;
            new Thread(() ->{
                try {
                    Thread.sleep((long)(2000 + new Random().nextDouble()));
                    System.out.println("thread " + finalI + " finished");
                } catch (InterruptedException exception) {
                    exception.printStackTrace();
                }
                latch.countDown();
            }).start();
        }
        latch.await();
        latch.await();
        System.out.println("all sub task finished!");
    }

2 await的源码剖析

上面已经演示了使用,下面来看看它的原理吧。

public class CountDownLatch {
  // 使用了AQS,不过是基于共享锁实现的 
    private static final class Sync extends AbstractQueuedSynchronizer {
        private static final long serialVersionUID = 4982264981922014374L;
      //这里实际上使用count作为了共享锁的state值
      // count数与共享锁的数量相同
      // 每调用一次countdown就是解一层锁
        Sync(int count) {
            setState(count);
        }
        int getCount() {
            return getState();
        }
      // 重写了共享锁的实现
      // 获取共享锁其实就是等待其它线程把state减到0
        protected int tryAcquireShared(int acquires) {
            return (getState() == 0) ? 1 : -1;
        }
      // 解锁的过程
        protected boolean tryReleaseShared(int releases) {
            // Decrement count; signal when transition to zero
            for (;;) {
                int c = getState();
                if (c == 0)
                    return false;
                int nextc = c-1;
                if (compareAndSetState(c, nextc))
                    return nextc == 0;
            }
        }
    }
    private final Sync sync;
    public CountDownLatch(int count) {
        if (count < 0) throw new IllegalArgumentException("count < 0");
        this.sync = new Sync(count); //构造sync方法
    }
  //通过acquireSharedInterruptibly获取共享锁,
  // 但是如果state不为0,将会被持续阻塞,后文详解
  public void await() throws InterruptedException {
        sync.acquireSharedInterruptibly(1);
    }
  // 同上,但是会被阻塞
    public boolean await(long timeout, TimeUnit unit)
        throws InterruptedException {
        return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
    }
  //countDown其实就是解锁一次
  public void countDown() {
        sync.releaseShared(1);
    }
// 获取当前的计数,也就是AQS的state值
  public long getCount() {
        return sync.getCount();
    }
  public String toString() {
        return super.toString() + "[Count = " + sync.getCount() + "]";
    }

我们终于知道CountDownLatch它的原理了,原来它就是利用共享锁来实现计数的,锁的数量就是计数数量,countdown的过程就是解锁的过程。


我们在该系列博客已经介绍过独占锁,但是没有深入剖析过共享锁,这里我们来深入共享锁的源码进行剖析,以便大家对CountDownLatch具有更为深入的理解。

点进AbstractQueuedSynchronizer类中。先看看acquireShared获取共享锁,这个就是CountDownLatchawait方法调用的底层方法(实际上是acquireSharedInterruptibly,不过原理是一样的)。

public final void acquireShared(int arg) {
      // 尝试获取共享锁,小于0则失败
        if (tryAcquireShared(arg) < 0)
          // 获取共享锁失败,进入阻塞
            doAcquireShared(arg);
}


相关文章
|
26天前
|
缓存 监控 Java
Java线程池提交任务流程底层源码与源码解析
【11月更文挑战第30天】嘿,各位技术爱好者们,今天咱们来聊聊Java线程池提交任务的底层源码与源码解析。作为一个资深的Java开发者,我相信你一定对线程池并不陌生。线程池作为并发编程中的一大利器,其重要性不言而喻。今天,我将以对话的方式,带你一步步深入线程池的奥秘,从概述到功能点,再到背景和业务点,最后到底层原理和示例,让你对线程池有一个全新的认识。
54 12
|
27天前
|
缓存 Java 开发者
Java多线程并发编程:同步机制与实践应用
本文深入探讨Java多线程中的同步机制,分析了多线程并发带来的数据不一致等问题,详细介绍了`synchronized`关键字、`ReentrantLock`显式锁及`ReentrantReadWriteLock`读写锁的应用,结合代码示例展示了如何有效解决竞态条件,提升程序性能与稳定性。
115 6
|
1月前
|
并行计算 数据处理 调度
Python中的并发编程:探索多线程与多进程的奥秘####
本文深入探讨了Python中并发编程的两种主要方式——多线程与多进程,通过对比分析它们的工作原理、适用场景及性能差异,揭示了在不同应用需求下如何合理选择并发模型。文章首先简述了并发编程的基本概念,随后详细阐述了Python中多线程与多进程的实现机制,包括GIL(全局解释器锁)对多线程的影响以及多进程的独立内存空间特性。最后,通过实例演示了如何在Python项目中有效利用多线程和多进程提升程序性能。 ####
|
28天前
|
设计模式 安全 Java
Java 多线程并发编程
Java多线程并发编程是指在Java程序中使用多个线程同时执行,以提高程序的运行效率和响应速度。通过合理管理和调度线程,可以充分利用多核处理器资源,实现高效的任务处理。本内容将介绍Java多线程的基础概念、实现方式及常见问题解决方法。
62 0
|
2月前
|
数据挖掘 程序员 调度
探索Python的并发编程:线程与进程的实战应用
【10月更文挑战第4天】 本文深入探讨了Python中实现并发编程的两种主要方式——线程和进程,通过对比分析它们的特点、适用场景以及在实际编程中的应用,为读者提供清晰的指导。同时,文章还介绍了一些高级并发模型如协程,并给出了性能优化的建议。
42 3
|
4天前
|
NoSQL Redis
单线程传奇Redis,为何引入多线程?
Redis 4.0 引入多线程支持,主要用于后台对象删除、处理阻塞命令和网络 I/O 等操作,以提高并发性和性能。尽管如此,Redis 仍保留单线程执行模型处理客户端请求,确保高效性和简单性。多线程仅用于优化后台任务,如异步删除过期对象和分担读写操作,从而提升整体性能。
16 1
|
2月前
|
存储 消息中间件 资源调度
C++ 多线程之初识多线程
这篇文章介绍了C++多线程的基本概念,包括进程和线程的定义、并发的实现方式,以及如何在C++中创建和管理线程,包括使用`std::thread`库、线程的join和detach方法,并通过示例代码展示了如何创建和使用多线程。
60 1
|
2月前
|
Java 开发者
在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口
【10月更文挑战第20天】在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口。本文揭示了这两种方式的微妙差异和潜在陷阱,帮助你更好地理解和选择适合项目需求的线程创建方式。
32 3
|
2月前
|
Java 开发者
在Java多线程编程中,选择合适的线程创建方法至关重要
【10月更文挑战第20天】在Java多线程编程中,选择合适的线程创建方法至关重要。本文通过案例分析,探讨了继承Thread类和实现Runnable接口两种方法的优缺点及适用场景,帮助开发者做出明智的选择。
25 2
|
2月前
|
Java
Java中多线程编程的基本概念和创建线程的两种主要方式:继承Thread类和实现Runnable接口
【10月更文挑战第20天】《JAVA多线程深度解析:线程的创建之路》介绍了Java中多线程编程的基本概念和创建线程的两种主要方式:继承Thread类和实现Runnable接口。文章详细讲解了每种方式的实现方法、优缺点及适用场景,帮助读者更好地理解和掌握多线程编程技术,为复杂任务的高效处理奠定基础。
41 2