【Java&Go并发编程系列】4.等待一组并发任务完成——CountDownLatch VS sync.WaitGroup

说明:Java & Go 并发编程序列的文章,根据每篇文章的主题或细分标题,分别演示 Java 和 Go 语言当中的相关实现。更多该系列文章请查看:Java & Go 并发编程系列

本文介绍 Java 和 Go 语言中如何实现并发编程中等待一组并发任务完成的场景。

代码场景:假设有一道题目,同时分发给10个同学来完成,所有同学完成之后,老师公布答案。

「Java」CountDownLatch

使用 CountDownLatch 等待一组并发任务的完成,包含如下三要素:

  • 一个初始值,即定义需要等待的任务的数目
  • await() 需要等待并发任务先完成的线程调用
  • countDown(),每个任务在完成的时候调用
public static void main(String[] args) throws InterruptedException {

    // 声明初始计数为10
    CountDownLatch countDownLatch = new CountDownLatch(10);

    // 起10个线程模拟10个同学做题目
    for (int i = 0; i < 10; i++) {
        Thread t = new Thread(() -> {
            // 生成从1-10的随机整数,模拟做题目花费的时间
            int randomSecond = ThreadLocalRandom.current().nextInt(10) + 1;
            try {
                TimeUnit.SECONDS.sleep(randomSecond);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.printf("第%s号同学用时%ds完成了题目\n", Thread.currentThread().getName(), randomSecond);
            // 计数减1
            countDownLatch.countDown();
        });
        // 设置线程名称,用于打印对应的同学编号
        t.setName(String.valueOf(i));
        t.start();
    }

    // 阻塞直到 CountDownLatch 的计数为0
    // 注意这里是 await 方法, 不是 wait 方法
    countDownLatch.await();
    System.out.printf("所有的同学都完成了题目,老师公布答案\n");
}

输出结果:

第2号同学用时2s完成了题目
第9号同学用时2s完成了题目
第8号同学用时2s完成了题目
第3号同学用时2s完成了题目
第5号同学用时4s完成了题目
第6号同学用时6s完成了题目
第1号同学用时8s完成了题目
第0号同学用时9s完成了题目
第4号同学用时9s完成了题目
第7号同学用时9s完成了题目
所有的同学都完成了题目,老师公布答案
「Go」sync.WaitGroup

使用 sync.WaitGroup 等待一组并发任务的完成,包含如下三要素:

  • Add(),传入一个 int 类型的参数,增加计数器的数值
  • Wait(),需要等待并发任务先完成的 goroutine 调用
  • Done(),每个任务在完成的时候调用
func main() {
	// sync.WaitGroup 是开箱即用的类型
	var wg sync.WaitGroup
	// 初始化计数器的值为10
	wg.Add(10)
	// 启用10个 goroutine 用于并发执行 go 函数 
	// goroutine,即用户级线程,也称协程,是 Go 语言并发编程模型中重要的三个元素 GPM 之一
	for i := 0; i < 10; i++ {
		go func(i int) {
			randomSecond := rand.Intn(10) + 1
			time.Sleep(time.Second * time.Duration(randomSecond))
			fmt.Printf("第%d号同学用时%ds完成了题目\n", i, randomSecond)
			// 计数器减1
			wg.Done()
		}(i)
	}
	// 阻塞直到所有并发任务执行完成
	wg.Wait()
	fmt.Printf("所有的同学都完成了题目,老师公布答案\n")
}

为了让程序的逻辑更严密,可以使用 defer 语句来保证 wg.Done() 一定被执行。

func main() {
	var wg sync.WaitGroup
	wg.Add(10)
	for i := 0; i < 10; i++ {
		go func(i int) {
			// defer 语句用于延迟执行代码,在函数正常或异常结束的时刻执行
			// 类似于 Java 的 finally
			defer wg.Done()
			randomSecond := rand.Intn(10) + 1
			time.Sleep(time.Second * time.Duration(randomSecond))
			fmt.Printf("第%d号同学用时%ds完成了题目\n", i, randomSecond)
		}(i)
	}
	wg.Wait()
	fmt.Printf("所有的同学都完成了题目,老师公布答案\n")
}

输出结果:

第2号同学用时1s完成了题目
第0号同学用时1s完成了题目
第6号同学用时2s完成了题目
第1号同学用时2s完成了题目
第8号同学用时6s完成了题目
第4号同学用时7s完成了题目
第5号同学用时8s完成了题目
第9号同学用时8s完成了题目
第7号同学用时9s完成了题目
第3号同学用时10s完成了题目
所有的同学都完成了题目,老师公布答案
拓展
「Java」CyclicBarrier

CyclicBarrier 可传入另一个 Runnable 对象,在所有线程都到达集合点后,将运行该 Runnable 对象,使用场景更广泛。

public static void main(String[] args) {

    // 声明10参与者和结束时要运行的 Runnable 对象
    // 这里只是打印了一行文字
    CyclicBarrier cyclicBarrier = new CyclicBarrier(10,
            () -> System.out.printf("所有的同学都完成了题目,老师公布答案\n")
    );

    // 起10个线程模拟10个同学做题目
    for (int i = 0; i < 10; i++) {
        Thread t = new Thread(() -> {
            // 生成从1-10的随机整数,模拟做题目花费的时间
            int randomSecond = ThreadLocalRandom.current().nextInt(10) + 1;
            try {
                TimeUnit.SECONDS.sleep(randomSecond);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.printf("第%s号同学用时%ds完成了题目\n", Thread.currentThread().getName(), randomSecond);
            try {
                // 阻塞直到所有参与者都调用 await 方法
                cyclicBarrier.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (BrokenBarrierException e) {
                e.printStackTrace();
            }
        });
        // 设置线程名称
        t.setName(String.valueOf(i));
        t.start();
    }

    System.out.printf("主线程执行结束\n");
}

输出结果:

主线程执行结束
第1号同学用时1s完成了题目
第5号同学用时1s完成了题目
第0号同学用时2s完成了题目
第3号同学用时4s完成了题目
第9号同学用时4s完成了题目
第2号同学用时5s完成了题目
第6号同学用时5s完成了题目
第4号同学用时6s完成了题目
第7号同学用时8s完成了题目
第8号同学用时10s完成了题目
所有的同学都完成了题目,老师公布答案

更多该系列文章请查看:Java & Go 并发编程系列

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,您想了解关于Java并发编程中的CountDownLatch吗? CountDownLatchJava并发编程中非常重要的一个工具类,它可以让某个线程等待其他线程完成后再继续执行。它的作用类似于倒计时门闩,当count值减为0时,所有等待者都会被唤醒。 CountDownLatch是一次性的,也就是说,它的计数器只能使用一次。如果需要重复使用,可以考虑使用CyclicBarrier。 在使用CountDownLatch时,我们需要先创建一个CountDownLatch对象,并指定计数器的初始值。然后在需要等待的线程中调用await()方法进行等待,同时在其他线程中调用countDown()方法进行计数器的减1操作。 举个例子,假设我们有一个需求:主线程需要等待两个子线程完成后再执行。那么可以这样编写代码: ```java import java.util.concurrent.CountDownLatch; public class CountDownLatchDemo { public static void main(String[] args) throws InterruptedException { CountDownLatch countDownLatch = new CountDownLatch(2); new Thread(() -> { System.out.println(Thread.currentThread().getName() + "执行开始"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "执行完毕"); countDownLatch.countDown(); }, "线程1").start(); new Thread(() -> { System.out.println(Thread.currentThread().getName() + "执行开始"); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "执行完毕"); countDownLatch.countDown(); }, "线程2").start(); System.out.println(Thread.currentThread().getName() + "等待子线程执行完毕"); countDownLatch.await(); System.out.println(Thread.currentThread().getName() + "所有子线程执行完毕,继续执行主线程"); } } ``` 在上面的例子中,我们首先创建了一个计数器初始值为2的CountDownLatch对象,然后创建了两个线程分别进行一些操作,并在操作结束后调用countDown()方法进行计数器减1操作。在主线程中,我们调用await()方法进行等待,直到计数器减为0时,主线程才会继续执行。 希望能够对您有所帮助!

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值