java线程并发控制同步工具CountDownLatch

目录
  • 前言
  • 了解CountDownLatch
    • 思考问题:
    • 主要参数与方法
    • 构造方法
  • CountDownLatch底层实现原理
  • 执行流程图
  • 实践
    • 用法一:
    • 用法二:
  • 总结

前言

大家好,我是小郭,前面我们学习了利用Semaphore来防止多线程同时操作一个资源,通常我们都会利用并行来优化性能,但是对于串行化的业务,可能需要按顺序执行,那我们怎么才能处理呢?今天我们来学习另一个并发流程控制的同步工具CountDownLatch。

了解CountDownLatch

首先,CountDownLatch是一种并发流程控制的同步工具。

主要的作用是等待多个线程同时完成任务之后,再继续完成主线程任务。

简单点可以理解为,几个小伙伴一起到火锅店聚餐,人到齐了,火锅店才可以开饭。

思考问题:

  • CountDownLatch 底层原理是什么,他是否可以代替wait / notify?
  • CountDwonLatch 业务场景有哪些?
  • 一次可以唤醒多个任务吗?

主要参数与方法

//减少锁存器的计数,如果计数达到零,则释放所有等待线程。
//计数器
public void countDown() {
    sync.releaseShared(1);
}
//导致当前线程等待,直到锁存器递减至零为止,除非该线程被中断。
//火锅店调用await的线程,count为0才能继续执行
public void await() throws InterruptedException {
    sync.acquireSharedInterruptibly(1);
}

构造方法

//count 数量,理解为小伙伴的个数
public CountDownLatch(int count) {
    if (count < 0) throw new IllegalArgumentException("count < 0");
        this.sync = new Sync(count);
}
//获取剩余的数量
public long getCount() {
    return sync.getCount();
}

CountDownLatch底层实现原理

我们可以看出countDown()是CountDownLatch的核心方法,我来看下他的具体实现。

CountDownLatch来时继承AQS的共享模式来完成其的实现,从前面的学习得出AQS主要是依赖同步队列和state实现控制。

共享模式:

这里与独占锁大多数相同,自旋过程中的退出条件是是当前节点的前驱节点是头结点并且tryAcquireShared(arg)返回值大于等于0即能成功获得同步状态.

await

public boolean await(long timeout, TimeUnit unit) throws InterruptedException {
    return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
}
//当状态不为0挂起,表示当前线程被占有,需要线程排队
protected int tryAcquireShared(int acquires) {
    return (getState() == 0) ? 1 : -1;
}
//在共享模式下获取
doAcquireSharedInterruptibly(int arg)

countDown

public void countDown() {
    sync.releaseShared(1);
}
protected boolean tryReleaseShared(int releases) {
// Decrement count; signal when transition to zero
//自旋防止失败
    for (;;) {
        //获取状态
        int c = getState();
        //状态为为0返回false,表示没有被线程占有
        if (c == 0) return false;
        //调用cas来进行替换,也保证了线程安全,当为0的时候唤醒
        int nextc = c-1;
        if (compareAndSetState(c, nextc))
            return nextc == 0;
    }
}
//当任务数量为0,aqs的释放共享锁
void doReleaseShared()
private void doReleaseShared() {
    /*
        * Ensure that a release propagates, even if there are other
        * in-progress acquires/releases.  This proceeds in the usual
        * way of trying to unparkSuccessor of head if it needs
        * signal. But if it does not, status is set to PROPAGATE to
        * ensure that upon release, propagation continues.
        * Additionally, we must loop in case a new node is added
        * while we are doing this. Also, unlike other uses of
        * unparkSuccessor, we need to know if CAS to reset status
        * fails, if so rechecking.
        */
    // 无限循环
    for (;;) {
        // 保存头节点
        Node h = head;
        // 头节点不为空并且头节点不为尾结点
        if (h != null && h != tail) {
            // 获取头节点的等待状态
            int ws = h.waitStatus;
            if (ws == Node.SIGNAL) {
                // 状态为SIGNAL,CAS更新状态
                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                    continue;            // loop to recheck cases
                // 释放后继结点
                unparkSuccessor(h);
            }
            // 状态为0并且更新不成功,继续
            else if (ws == 0 &&
                        !compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) //
                continue;                // loop on failed CAS
        }
        if (h == head) // 若头节点改变,继续循环
            break;
    }
}

思考

  • 如何安排线程排序

个人认为,没有进行线程的排序,而是让一部分线程进入等待,在唤醒的时候放开。

执行流程图

实践

用法一:

一个线程等待其他多个线程都执行完毕,再继续自己的工作

public class CountDownLatchTest {
    private static Lock lock = new ReentrantLock();
    private static CountDownLatch countDownLatch = new CountDownLatch(4);
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(4);
        IntStream.range(0,16).forEach(i -&gt;{
            executorService.submit(()-&gt;{
                lock.lock();
                System.out.println(Thread.currentThread().getName()+ "来火锅店吃火锅!");
                try {
                    Thread.sleep(1000);
                    countDownLatch.countDown();
                    System.out.println(Thread.currentThread().getName() + "我到火锅店了,准备开吃!");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    lock.unlock();
                }
            });
        });
        try {
            countDownLatch.await(5,TimeUnit.SECONDS);
            System.out.println("人到齐了,开饭");
            executorService.shutdown();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

输出结果

代码中设置了一个CountDownLatch做倒计时,四个人(count为4)一起到火锅店吃饭,每到一个人计数器就减去1(countDownLatch.countDown()),当计数器为0的时候,main线程在await的阻塞结束,继续往下执行。

用法二:

多个线程等待某一个线程的信号,同时开始执行

用抢位子作为例子,将线程挂起等待,同时开始执行。

public class CountDownLatchTest2 {
    private static Lock lock = new ReentrantLock();
    private static CountDownLatch countDownLatch = new CountDownLatch(1);
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(4);
        IntStream.range(0,4).forEach(i ->{
            executorService.submit(()->{
                System.out.println(Thread.currentThread().getName()+ "准备开始抢位子!");
                try {
                    //Thread.sleep(1000);
                    countDownLatch.await();
                    System.out.println(Thread.currentThread().getName() + "抢到了位置");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        });
        try {
            Thread.sleep(5000);
            System.out.println("五秒后开始抢位置");
            countDownLatch.countDown();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        executorService.shutdown();
    }
}

注意点

CountDownLatch是不能重用的。

总结

我们可以看到CountDownLatch的使用很简单,就当做一个计时器来使用,在控制并发方面能给我们提供帮助。

  • 在构造器中初始化任务数量
  • 调用await()挂起主线程main
  • 调用countDown()方法减一,直到为0的时候,唤醒主线程可以继续运行。

上面提供的两个用法,我们也可以结合起来使用。

在实际的业务代码开发中,利用CountDownLatch来进行业务方法的执行,来确定他们的顺序,解决一个线程等待多个线程的场景

以上就是java线程并发控制同步工具CountDownLatch的详细内容,更多关于java线程并发CountDownLatch的资料请关注我们其它相关文章!

时间: 2022-08-09

Java并发编程:CountDownLatch与CyclicBarrier和Semaphore的实例详解

Java并发编程:CountDownLatch与CyclicBarrier和Semaphore的实例详解 在java 1.5中,提供了一些非常有用的辅助类来帮助我们进行并发编程,比如CountDownLatch,CyclicBarrier和Semaphore,今天我们就来学习一下这三个辅助类的用法. 以下是本文目录大纲: 一.CountDownLatch用法 二.CyclicBarrier用法 三.Semaphore用法 若有不正之处请多多谅解,并欢迎批评指正. 一.CountDownLatch

Java并发系列之CountDownLatch源码分析

CountDownLatch(闭锁)是一个很有用的工具类,利用它我们可以拦截一个或多个线程使其在某个条件成熟后再执行.它的内部提供了一个计数器,在构造闭锁时必须指定计数器的初始值,且计数器的初始值必须大于0.另外它还提供了一个countDown方法来操作计数器的值,每调用一次countDown方法计数器都会减1,直到计数器的值减为0时就代表条件已成熟,所有因调用await方法而阻塞的线程都会被唤醒.这就是CountDownLatch的内部机制,看起来很简单,无非就是阻塞一部分线程让其在达到某个条

Java线程并发工具类CountDownLatch原理及用法

一.CountDownLatch [1]CountDownLatch是什么? CountDownLatch,英文翻译为倒计时锁存器,是一个同步辅助类,在完成一组正在其他线程中执行的操作之前,它允许一个或 多个线程一直等待. 闭锁可以延迟线程的进度直到其到达终止状态,闭锁可以用来确保某些活动直到其他活动都完成才继续执行: 确保某个计算在其需要的所有资源都被初始化之后才继续执行; 确保某个服务在其依赖的所有其他服务都已经启动之后才启动; 等待直到某个操作所有参与者都准备就绪再继续执行: CountD

java线程并发countdownlatch类使用示例

复制代码 代码如下: package com.yao; import java.util.concurrent.CountDownLatch;import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors; /** * CountDownLatch是个计数器,它有一个初始数, * 等待这个计数器的线程必须等到计数器倒数到零时才可继续. */public class CountDownLatchTe

java并发包中CountDownLatch和线程池的使用详解

1.CountDownLatch 现在做的这个华为云TaurusDB比赛中,参考的之前参加过阿里的PolarDB大赛的两个大佬的代码,发现都有用到CountDownLatch这个类,之前看代码的时候也看过,但是没有搞得很明白,自己写也写不出来,在此自己先学习一下. 字面理解:CountDownLatch:数量减少的门栓. 创建这样一个门栓 CountDownLatch countDownLatch = new CountDownLatch(count); 参数:count,门栓的计数次数. 在所

java并发使用CountDownLatch在生产环境翻车剖析

目录 前言 需求背景 具体实现 解决方案 总结 前言 大家好,我是小郭,之前分享了CountDownLatch的使用,我们知道用来控制并发流程的同步工具,主要的作用是为了等待多个线程同时完成任务后,在进行主线程任务. 万万没想到,在生产环境中竟然翻车了,因为没有考虑到一些场景,导致了CountDownLatch出现了问题,接下来来分享一下由于CountDownLatch导致的问题. [线程]并发流程控制的同步工具-CountDownLatch 需求背景 先简单介绍下业务场景,针对用户批量下载的文

java并发编程专题(八)----(JUC)实例讲解CountDownLatch

CountDownLatch 是一个非常实用的多线程控制工具类." Count Down " 在英文中意为倒计数, Latch 为门问的意思.如果翻译成为倒计数门阀, 我想大家都会觉得不知所云吧! 因此,这里简单地称之为倒计数器.在这里, 门问的含义是:把门锁起来,不让里面的线程跑出来.因此,这个工具通常用来控制线程等待,它可以让某一个线程等待直到倒计时结束, 再开始执行. CountDown Latch 的构造函数接收一个整数作为参数,即当前这个计数器的计数个数. public Co

java并发等待条件的实现原理详解

前言 前面介绍了排它锁,共享锁的实现机制,本篇继续学习AQS中的另外一个内容-Condition.想必学过java的都知道Object.wait和Object.notify,同时也应该知晓这两个方法的使用离不开synchronized关键字.synchronized是jvm级别提供的同步原语,它的实现机制隐藏在jvm实现中.作为Lock系列功能中的Condition,就是用来实现类似 Object.wait和Object.notify 对应功能的. 使用场景 为了更好的理解Lock和Condit

Java并发系列之AbstractQueuedSynchronizer源码分析(概要分析)

学习Java并发编程不得不去了解一下java.util.concurrent这个包,这个包下面有许多我们经常用到的并发工具类,例如:ReentrantLock, CountDownLatch, CyclicBarrier, Semaphore等.而这些类的底层实现都依赖于AbstractQueuedSynchronizer这个类,由此可见这个类的重要性.所以在Java并发系列文章中我首先对AbstractQueuedSynchronizer这个类进行分析,由于这个类比较重要,而且代码比较长,为了

Java并发系列之ConcurrentHashMap源码分析

我们知道哈希表是一种非常高效的数据结构,设计优良的哈希函数可以使其上的增删改查操作达到O(1)级别.Java为我们提供了一个现成的哈希结构,那就是HashMap类,在前面的文章中我曾经介绍过HashMap类,知道它的所有方法都未进行同步,因此在多线程环境中是不安全的.为此,Java为我们提供了另外一个HashTable类,它对于多线程同步的处理非常简单粗暴,那就是在HashMap的基础上对其所有方法都使用synchronized关键字进行加锁.这种方法虽然简单,但导致了一个问题,那就是在同一时间

Java并发系列之ReentrantLock源码分析

在Java5.0之前,协调对共享对象的访问可以使用的机制只有synchronized和volatile.我们知道synchronized关键字实现了内置锁,而volatile关键字保证了多线程的内存可见性.在大多数情况下,这些机制都能很好地完成工作,但却无法实现一些更高级的功能,例如,无法中断一个正在等待获取锁的线程,无法实现限定时间的获取锁机制,无法实现非阻塞结构的加锁规则等.而这些更灵活的加锁机制通常都能够提供更好的活跃性或性能.因此,在Java5.0中增加了一种新的机制:Reentrant

Java并发编程包中atomic的实现原理示例详解

线程安全: 当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些进程将如何交替执行,并且在主调代码中不需要任何额外的同步或协调,这个类都能表现出正确的行为,那么就称这个类时线程安全的. 线程安全主要体现在以下三个方面: 原子性:提供了互斥访问,同一时刻只能有一个线程对它进行操作 可见性:一个线程对主内存的修改可以及时的被其他线程观察到 有序性:一个线程观察其他线程中的指令执行顺序,由于指令重排序的存在,该观察结果一般杂乱无序 引子 在多线程的场景中,我们需要保证数据安全,就会考虑同步的

浅谈Java并发编程基础知识

进程和线程 在并行程序中进程和线程是两个基本的运行单元,在Java并发编程中,并发主要核心在于线程 1. 进程 一个进程有其专属的运行环境,一个进程通常有一套完整.私有的运行时资源:尤其是每个进程都有其专属的内存空间. 通常情况下,进程等同于运行的程序或者应用,然而很多情况下用户看到的一个应用实际上可能是多个进程协作的.为了达到进程通信的目的,主要的操作系统都实现了Inter Process Communication(IPC)资源,例如pipe和sockets,IPC不仅能支持同一个系统中的进

java并发编程专题(一)----线程基础知识

在任何的生产环境中我们都不可逃避并发这个问题,多线程作为并发问题的技术支持让我们不得不去了解.这一块知识就像一个大蛋糕一样等着我们去分享,抱着学习的心态,记录下自己对并发的认识. 1.线程的状态: 线程状态图: 1.新建状态(New):新创建了一个线程对象. 2.就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start()方法.该状态的线程位于可运行线程池中,变得可运行,等待获取CPU的使用权. 3.运行状态(Running):就绪状态的线程获取了CPU,执行程序代码. 4