CountDownLatch和CyclicBarrier详解

1. CountDownLatch

1.1 简介

CountDownLatch 是 Java 中并发包(java.util.concurrent)提供的一种同步工具,用于在多线程环境中协调多个线程之间的执行顺序。它的作用是允许一个或多个线程等待其他线程完成操作。

CountDownLatch 通过一个计数器来实现,计数器的初始值由用户设置,每当一个线程完成一项任务后,计数器的值就会减一。当计数器的值变为零时,等待在 CountDownLatch 上的线程就会被唤醒,可以继续执行。

以下是 CountDownLatch 的一些关键方法:

  1. CountDownLatch(int count): 构造方法,传入计数器的初始值。

  2. void countDown(): 计数器减一,表示一个线程完成了任务。

  3. void await(): 等待计数器变为零,阻塞当前线程。

  4. boolean await(long timeout, TimeUnit unit): 在指定的时间内等待计数器变为零,超时后返回 true,否则返回 false。

以下是一个简单的示例,演示了 CountDownLatch 的基本用法:

import java.util.concurrent.CountDownLatch;

public class CountDownLatchExample {
    public static void main(String[] args) throws InterruptedException {
        int numThreads = 3;
        CountDownLatch latch = new CountDownLatch(numThreads);

        for (int i = 0; i < numThreads; i++) {
            new Thread(() -> {
                // 模拟线程执行任务
                System.out.println("Thread " + Thread.currentThread().getId() + " is executing.");
                latch.countDown(); // 任务完成,计数器减一
            }).start();
        }

        latch.await(); // 等待计数器变为零
        System.out.println("All threads have completed their tasks.");
    }
}

在这个示例中,创建了一个 CountDownLatch,并在多个线程中模拟执行任务,每个线程执行完任务后调用 countDown 方法。主线程通过 await 方法等待所有线程执行完任务后继续执行。这样,CountDownLatch 就实现了多个线程之间的协调。

CountDownLatch 的应用场景包括等待多个线程完成某个任务后再进行下一步操作、并行计算中等待多个计算任务完成、主线程等待多个子线程初始化完成等。

1.2 使用CountDownLatch模拟王者荣耀加载页面

package com.test;

import java.util.Arrays;
import java.util.Random;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * @author 曹见朋
 * @create 2023-11-12-15:38
 */
public class CountDownLatchTest {

    public static void main(String[] args) {

        ExecutorService executorService = Executors.newFixedThreadPool(5);

        CountDownLatch countDownLatch = new CountDownLatch(5);

        Random random = new Random();

        String[] all = new String[5];


        for (int i = 0; i < 5; i++) {
            int k=i;
            executorService.submit(()->{
                for (int j = 0; j <= 100; j++) {
                    try {
                        Thread.sleep(random.nextInt(100));
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    all[k] = j+"%";
                    System.out.print("\r"+ Arrays.toString(all));
                }
                countDownLatch.countDown();
            });
        }


        try {
            System.out.println("玩家正在进入游戏......游戏即将开始......");
            System.out.println();
            countDownLatch.await();
            System.err.println("\n"+"\n"+"游戏开始!敌人还有3秒到达!请做好准备!");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            executorService.shutdown();
        }
    }
}

CountDownLatch和CyclicBarrier详解_第1张图片

2. CyclicBarrier

2.1 简介

CyclicBarrier 是 Java 中并发包(java.util.concurrent)提供的另一种同步工具,用于在多线程环境中实现多个线程的同步点。与 CountDownLatch 类似,CyclicBarrier 也可以用于线程的协同,但其机制略有不同。

CyclicBarrier 的主要特点是可以重用。它允许一组线程相互等待,直到所有线程都达到某个同步点,然后继续执行。一旦所有线程达到同步点,CyclicBarrier 的内部计数器会被重置,并且所有线程可以继续执行下一轮同步。

以下是 CyclicBarrier 的一些关键方法:

  1. CyclicBarrier(int parties): 构造方法,传入参与同步的线程数。

  2. int await(): 调用线程到达同步点,等待其他线程。当所有线程都调用了 await 方法后,它们就会被释放,并且 CyclicBarrier 的内部计数器会被重置。

  3. int await(long timeout, TimeUnit unit): 在指定的时间内等待其他线程。如果在超时时间内没有所有线程都到达同步点,调用线程会被释放,并返回一个表示等待状态的值。

以下是一个简单的示例,演示了 CyclicBarrier 的基本用法:

import java.util.concurrent.CyclicBarrier;

public class CyclicBarrierExample {
    public static void main(String[] args) {
        int numThreads = 3;
        CyclicBarrier barrier = new CyclicBarrier(numThreads, () -> {
            // 当所有线程都到达同步点时执行的动作
            System.out.println("All threads have reached the barrier.");
        });

        for (int i = 0; i < numThreads; i++) {
            new Thread(() -> {
                try {
                    // 模拟线程执行任务
                    System.out.println("Thread " + Thread.currentThread().getId() + " is executing.");
                    barrier.await(); // 等待其他线程到达同步点
                    System.out.println("Thread " + Thread.currentThread().getId() + " continues execution.");
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}

在这个示例中,创建了一个 CyclicBarrier,并在多个线程中模拟执行任务。每个线程执行完任务后调用 await 方法,等待其他线程到达同步点。一旦所有线程都到达同步点,就会执行在构造方法中传入的动作,并且所有线程会被释放,可以继续执行下一轮同步。

CyclicBarrier 的应用场景包括多个线程分阶段执行任务、多个线程分工合作执行任务等。

2.2 CyclicBarrier 模拟游戏多人联机对战场景

在游戏开发中,CyclicBarrier 可以用于实现多个玩家或角色在某个关键点进行同步,以确保所有玩家都准备好后再开始某个阶段的游戏。以下是一个典型的应用场景:

游戏多人联机对战场景:

假设有一个多人联机对战游戏,游戏中的每个玩家都需要准备好装备、选择角色等信息后,才能开始游戏。使用 CyclicBarrier 可以在游戏开始前创建一个同步点,确保所有玩家都准备就绪后才开始游戏。

package com.test.mythreadpool;

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

/**
 * @author 曹见朋
 * @create 2023-11-12-16:27
 */
public class CyclicBarrierTest {

    private static final int NUM_PLAYERS = 4;
    private static final CyclicBarrier gameStartBarrier = new CyclicBarrier(NUM_PLAYERS, () -> {
        System.out.println("All players are ready. Game starts!");
    });

    public static void main(String[] args) {
        for (int i = 0; i < NUM_PLAYERS; i++) {
            new Thread(() -> {
                // 模拟玩家准备工作
                System.out.println("Player " + Thread.currentThread().getId() + " is preparing.");
                try {
                    // 等待其他玩家准备
                    gameStartBarrier.await();
                    // 开始游戏
                    System.out.println("Player " + Thread.currentThread().getId() + " starts the game.");
                } catch (InterruptedException | BrokenBarrierException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}


在这个例子中,CyclicBarrier 被用于创建一个同步点 gameStartBarrier,在这个同步点上,所有玩家等待,直到所有玩家都准备好后,执行了 gameStartBarrier.await() 后,所有玩家同时开始游戏。

这种应用场景确保了游戏开始前所有玩家的同步,避免了一些玩家在游戏开始后还在准备中的问题,提升了游戏的整体体验。

3. CyclicBarrier和CountDownLatch的区别是什么?

CyclicBarrier 和 CountDownLatch 都是Java并发包提供的用于多线程协同的工具,但它们有一些关键区别:

  1. 可重用性:

    • CyclicBarrier 是可重用的。一旦所有线程到达同步点,CyclicBarrier 的内部计数器会被重置,可以用于下一轮的同步。
    • CountDownLatch 是不可重用的。一旦计数器减为零,无法重新设置计数器,而且无法再次使用。
  2. 计数器递减:

    • CyclicBarrier 的计数器是递增的。线程调用 await 方法等待,计数器递增,直到计数器达到设定值时,所有等待的线程被唤醒。
    • CountDownLatch 的计数器是递减的。每次调用 countDown 方法,计数器减一,当计数器减为零时,等待的线程被唤醒。
  3. 等待状态:

    • 在 CyclicBarrier 中,等待的线程在同步点处被阻塞,一旦所有线程都到达同步点,它们被同时释放,继续执行下一步操作。
    • 在 CountDownLatch 中,等待的线程在计数器为零之前一直被阻塞,一旦计数器为零,所有等待的线程被唤醒,继续执行。
  4. 动作:

    • CyclicBarrier 允许在所有线程到达同步点时执行一个可选的动作(Runnable)。
    • CountDownLatch 不提供类似的机制。

下面是一个简单的比较例子:

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.CyclicBarrier;

public class ComparisonExample {
    public static void main(String[] args) throws InterruptedException {
        int numThreads = 3;

        // CyclicBarrier 示例
        CyclicBarrier cyclicBarrier = new CyclicBarrier(numThreads, () -> {
            System.out.println("CyclicBarrier: All threads have reached the barrier.");
        });

        for (int i = 0; i < numThreads; i++) {
            new Thread(() -> {
                System.out.println("CyclicBarrier: Thread is performing a task.");
                try {
                    cyclicBarrier.await();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }).start();
        }

        Thread.sleep(2000); // 等待线程执行完成

        // CountDownLatch 示例
        CountDownLatch countDownLatch = new CountDownLatch(numThreads);

        for (int i = 0; i < numThreads; i++) {
            new Thread(() -> {
                System.out.println("CountDownLatch: Thread is performing a task.");
                countDownLatch.countDown();
            }).start();
        }

        countDownLatch.await(); // 等待计数器为零

        System.out.println("CountDownLatch: All threads have completed their tasks.");
    }
}

在这个例子中,通过 CyclicBarrier 和 CountDownLatch 分别创建了两组线程,模拟了它们的使用方式。CyclicBarrier 在所有线程到达同步点时执行一个动作,而 CountDownLatch 在计数器为零时唤醒等待的线程。

最后,让我们以旅行团的情景来说明 CyclicBarrier 和 CountDownLatch 的区别:

  • CyclicBarrier(循环屏障)
    假设有一个旅行团,旅行中有多个景点需要游客参观。在出发前,导游告诉游客,每到一个景点,所有游客都需要等待其他游客到达后才能一起继续前进。导游设置了一个 CyclicBarrier,每个景点都是一个同步点。当游客到达一个景点后,他们调用 await 方法等待其他游客到达。一旦所有游客都到达,CyclicBarrier 就会打开,所有游客一起前往下一个景点。
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;

public class TravelGroupWithCyclicBarrier {
    private static final int NUM_TOURISTS = 5;
    private static final CyclicBarrier arrivalBarrier = new CyclicBarrier(NUM_TOURISTS, () -> {
        System.out.println("All tourists have arrived. Moving to the next attraction.");
    });

    public static void main(String[] args) {
        for (int i = 0; i < NUM_TOURISTS; i++) {
            new Thread(() -> {
                // 游客到达当前景点
                System.out.println("Tourist " + Thread.currentThread().getId() + " has arrived.");
                try {
                    // 等待其他游客到达
                    arrivalBarrier.await();
                } catch (InterruptedException | BrokenBarrierException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}

  • CountDownLatch
    现在,假设旅行团中的每个游客都有自己的一些准备工作,例如检查护照、购买旅行用品等。在出发前,导游告诉游客,所有准备工作完成后才能出发。导游设置了一个 CountDownLatch,初始计数器为旅客的数量。每个游客在完成准备工作后,调用 countDown 方法减少计数器。导游在等待计数器减为零后,就知道所有游客都准备好了,可以出发了。
import java.util.concurrent.CountDownLatch;

public class TravelGroupWithCountDownLatch {
    private static final int NUM_TOURISTS = 5;
    private static final CountDownLatch preparationLatch = new CountDownLatch(NUM_TOURISTS);

    public static void main(String[] args) {
        for (int i = 0; i < NUM_TOURISTS; i++) {
            new Thread(() -> {
                // 游客完成准备工作
                System.out.println("Tourist " + Thread.currentThread().getId() + " has completed preparations.");
                // 减少准备计数器
                preparationLatch.countDown();
            }).start();
        }

        try {
            // 等待所有游客完成准备工作
            preparationLatch.await();
            System.out.println("All tourists have completed preparations. Departing now!");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

  • 区别总结
    • CyclicBarrier 适用于一组线程需要相互等待,达到同一点后再继续执行的场景,而且可以重复使用。
    • CountDownLatch 适用于一个或多个线程需要等待其他线程完成某个操作后再执行的场景,但它是一次性的,计数器归零后不能再次使用。

你可能感兴趣的:(JUC,并发编程)