CountDownLatch与CyclicBarrier详解


前言

本文介绍CountDownLatch与CyclicBarrier,以及其中容易发生的问题。


一、CountDownLatch

介绍

CountDownLatch(倒计时门栓)是Java并发包中的一个工具类,用于在多线程编程中实现线程间的等待。它允许一个或多个线程等待其他线程完成操作后再继续执行。

用法详解

初始化CountDownLatch

CountDownLatch latch = new CountDownLatch(N);

其中,N 是需要等待完成的操作数量。当 N 个操作完成后,通过 countDown() 方法递减计数器,latch 的 await() 方法将会返回。

主要方法

countDown()

该方法使计数器减 1。当某个线程完成了自己的任务,调用此方法通知计数器减 1。

await()

调用此方法会使当前线程等待,直到计数器的值变为 0。一旦计数器为 0,等待的线程会继续执行。

示例详情

import java.util.concurrent.CountDownLatch;

public class Main {
    public static void main(String[] args) throws InterruptedException {
        final int N = 3;
        CountDownLatch latch = new CountDownLatch(N);
        
        // 创建 N 个线程执行任务
        for (int i = 0; i < N; i++) {
            Thread thread = new Thread(new Worker(i, latch));
            thread.start();
        }
        
        // 等待所有线程执行完毕
        latch.await();
        System.out.println("所有线程已执行完成,开始执行后续逻辑业务");
    }
}

class Worker implements Runnable {
    private final int id;
    private final CountDownLatch latch;
    
    public Worker(int id, CountDownLatch latch) {
        this.id = id;
        this.latch = latch;
    }
    
    @Override
    public void run() {
        System.out.println("线程" + id + "开始执行 ");
        try {
            Thread.sleep(2000); // 模拟任务耗时执行
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("线程" + id + " 执行完毕 ");
        latch.countDown(); // 完成任务,计数器减 1
    }
}

使用场景

1.主线程等待多个子线程全部执行完成后再继续执行。
2.控制并发线程数量,限制同时执行的线程数。

CountDownLatch 是一个非常有用的工具类,在并发编程中常被使用,特别是在多线程协作的场景中。

注意事项

虽然 CountDownLatch 是一个强大且有用的工具,但在使用时可能会遇到一些问题,特别是在不正确使用或不适当处理异常的情况下。以下是一些可能出现的问题:

1.未正确处理异常: 在使用 CountDownLatch 时,如果不正确处理线程可能抛出的异常,可能会导致某些线程提前退出而无法执行 countDown() 方法,进而导致主线程一直等待,无法继续执行。

2.死锁: 如果计数器的值不能达到零,主线程会一直等待,这可能会导致死锁。这可能是因为某些线程没有正确调用 countDown() 方法,或者计数器的值设置不正确。

3.计数器过早到达零: 如果在主线程调用 await() 方法之前,已经有线程调用了足够数量的 countDown() 方法,那么主线程将不会等待,可能导致在预期之前继续执行。

4.性能问题: 在高并发环境中,如果使用 CountDownLatch 进行大量的线程同步,可能会导致性能问题,因为所有线程都需要等待计数器归零才能继续执行。

5.不恰当的设计: 在某些情况下,可能会更适合使用其他同步工具,如 CyclicBarrier 或 Semaphore,而不是 CountDownLatch。如果选择了不合适的同步机制,可能会导致设计不够灵活或者性能不佳。

为避免这些问题,应该仔细设计并正确使用 CountDownLatch,并确保在适当的时候处理异常情况,以及在主线程和子线程之间进行合适的同步。


二、CyclicBarrier

介绍

CyclicBarrier(循环屏障)是 Java 并发包中的一个同步工具类,用于实现多个线程之间的同步。它允许一组线程相互等待,直到所有线程都达到某个共同的屏障点,然后继续执行。

用法详解

初始化CyclicBarrier

CyclicBarrier barrier = new CyclicBarrier(int parties, Runnable barrierAction);

parties 是参与线程的数量,即需要等待的线程数量。
barrierAction 是当所有线程到达屏障点时执行的操作。可以为 null。

主要方法

await()

当线程到达屏障点时调用,导致该线程等待,直到所有参与线程都到达屏障点。

reset()

将屏障重置为初始状态,用于重复使用。

示例详情

考虑一个场景,有一群游客参加一项活动,需要等待所有游客到齐后才能开始游戏。

import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;

public class Main {
    public static void main(String[] args) {
        final int numPlayers = 5;
        Runnable gameStartAction = () -> System.out.println("所有玩家已到齐,游戏开始!");
        
        CyclicBarrier barrier = new CyclicBarrier(numPlayers, gameStartAction);
        for (int i = 0; i < numPlayers; i++) {
            Thread playerThread = new Thread(new Player(i, barrier));
            playerThread.start();
        }
    }
}

class Player implements Runnable {
    private final int playerId;
    private final CyclicBarrier barrier;

    public Player(int playerId, CyclicBarrier barrier) {
        this.playerId = playerId;
        this.barrier = barrier;
    }

    @Override
    public void run() {
        System.out.println("玩家 " + playerId + " 已到达...");
        try {
            barrier.await(); // 等待所有玩家到齐
            System.out.println("玩家 " + playerId + " 开始游戏!");
        } catch (InterruptedException | BrokenBarrierException e) {
            e.printStackTrace();
        }
    }
}

使用场景

1.并行任务分解:将任务分解为多个子任务,并让多个线程并行执行这些子任务,最后再将结果合并。
2.控制并发阶段:在多个阶段的任务中,每个阶段需要等待所有线程完成后才能继续执行。
3.数据计算并行化:当数据处理任务可以被分解为多个独立的子任务时,可以使用 CyclicBarrier 来同步这些子任务的执行。

CyclicBarrier 提供了一种简单且强大的同步机制,能够有效地控制多个线程之间的协作。

注意事项

虽然 CyclicBarrier 是一个有用的同步工具,但在使用时可能会遇到一些问题。以下是一些可能出现的问题:

1.死锁: 如果在线程到达屏障点之前发生异常,并且没有正确处理,可能会导致线程永远等待,从而引发死锁。

2.线程数量不匹配: 如果在初始化 CyclicBarrier 时指定的参与线程数量与实际线程数量不匹配,可能会导致一些线程永远等待,或者在一些线程已经到达屏障点时其他线程仍在等待。

3.超时问题: CyclicBarrier 不支持超时,如果线程因某种原因无法到达屏障点,其他线程将永远等待。如果需要超时功能,可以考虑使用 CountDownLatch 或 Semaphore。

4.内存泄漏: 如果在使用完毕后没有显式调用 reset() 方法重置 CyclicBarrier,可能会导致内存泄漏问题,因为 CyclicBarrier 内部会持有对线程的引用。

5.屏障动作异常: 如果在初始化 CyclicBarrier 时指定了屏障动作,并且该动作抛出了异常,可能会导致其他线程无法正常执行。

为避免这些问题,应该在使用 CyclicBarrier 时仔细考虑可能出现的异常情况,并且确保正确处理。特别是,在多线程环境中,应该正确处理线程可能抛出的异常,以避免导致程序不稳定或者死锁等问题的发生。


三、CountDownLatch与CyclicBarrier区别

  1. 使用次数
    CyclicBarrier 可以重用,当一组线程到达屏障点后,它会重置并等待下一次使用。
    CountDownLatch 只能使用一次,一旦计数器归零,就无法重置或再次使用。
  2. 等待条件
    CyclicBarrier 用于一组线程彼此等待,直到所有线程都到达一个共同的屏障点,然后继续执行。
    CountDownLatch 用于一个或多个线程等待另一组线程执行完特定操作后再继续执行。
  3. 计数器类型
    CyclicBarrier 内部的计数器是递增的,每个线程调用 await() 方法时计数器加一,直到计数器达到预设的值时释放所有等待线程。
    CountDownLatch 内部的计数器是递减的,初始化时设置一个初始值,每次调用 countDown() 方法计数器减一,直到计数器为零时释放所有等待线程。
  4. 屏障点动作
    CyclicBarrier 可以在所有线程到达屏障点时执行一个可选的屏障动作。
    CountDownLatch 没有屏障动作。
  5. 重置能力
    CyclicBarrier 可以通过 reset() 方法重置屏障,以便在之后的使用中重新初始化。
    CountDownLatch 不能重置,一旦计数器归零,就无法再次使用。
  6. 异常处理
    CyclicBarrier 的等待线程可以抛出 BrokenBarrierException 异常,表示某个等待线程在等待过程中被中断或者超时。
    CountDownLatch 的等待线程无法抛出异常。

总的来说,CyclicBarrier 适用于一组线程相互等待,然后同时执行某个操作,而 CountDownLatch 适用于一个或多个线程等待其他一组线程完成某个操作后继续执行。

  • 19
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值