📝个人主页🌹:个人主页
⏩收录专栏⏪:JAVA进阶
🌹🌹期待您的关注 🌹🌹,让我们共同进步!
文章目录
在JDK的并发包中,有几个非常有用的并发工具类,本文主要来讲讲其中CountDownLatch
简介
CountDownLatch是一个同步工具类,它允许一个或多个线程等待其他线程完成操作。CountDownLatch用一个给定的计数器来初始化,该计数器的值表示需要等待完成的任务数量。每当一个线程完成其任务后,计数器的值就会减一。当计数器的值达到零时,表示所有需要等待的任务都已经完成,此时在CountDownLatch上等待的线程将被唤醒并可以继续执行。
基本工作原理
CountDownLatch内部维护了一个计数器,只有当计数器==0时,某些线程才会停止阻塞,开始执行。
● CountDownLatch主要有两个方法,当一个或多个线程调用await方法时,这些线程会阻塞。
● 其它线程调用countDown方法会将计数器减1(调用countDown方法的线程不会阻塞),当计数器的值变为0时,因await方法阻塞的线程会被唤醒,继续执行。
AQS是一个用于构建锁和其他同步组件的基础框架。它使用一个整型的state字段来表示同步状态,并提供了一系列的方法来操作这个状态。AQS内部维护了一个FIFO的队列,用于管理等待获取同步状态的线程。
CountDownLatch内部组成
CountDownLatch内部主要由一个计数器、一个等待队列以及相关的同步控制逻辑组成。
1. 计数器(Counter)
- 作用:CountDownLatch内部使用一个计数器来跟踪还需要等待完成的操作数量。这个计数器的值在创建CountDownLatch对象时通过构造函数初始化,代表需要等待的线程数量。
- 特点:计数器的值随着线程完成操作而递减,当计数器的值减至0时,表示所有等待的操作已完成。
2. 等待队列(Waiting Queue)
- 作用:CountDownLatch内部维护一个等待队列,用于存放那些因为计数器未归零而阻塞的线程。当计数器的值减至0时,这些线程将被唤醒并继续执行。
- 特点:等待队列通常采用先进先出(FIFO)的策略来管理线程,确保线程被唤醒的顺序性。
3. 同步控制逻辑(Synchronization Control Logic)
- 作用:负责协调线程的等待与唤醒过程,确保在多线程环境下计数器的更新和线程的唤醒操作是安全的、一致的。
- 特点:
A: 原子性:使用CAS(Compare-And-Swap)等原子操作来更新计数器的值,确保在并发环境下计数器的值不会被错误地修改。
B: 锁与条件变量:内部可能使用锁(如ReentrantLock)和条件变量(Condition)来实现线程的阻塞与唤醒。
C: 响应中断:等待线程可以被中断,并在中断时抛出InterruptedException异常。4. 状态管理(State Management)
- 作用:管理CountDownLatch的内部状态,包括计数器的值、等待队列中线程的状态等。
- 特点:
- 状态不可重置:一旦CountDownLatch的计数器归零,它就不能再被重置或重新使用。如果需要多次重复利用类似的同步机制,应该考虑使用CyclicBarrier等其他工具。
- 线程安全性:确保在多线程环境下CountDownLatch的状态不会被破坏,所有线程看到的都是一致的状态视图。
5. 超时机制(Timeout Mechanism)
- 作用:除了无参的await()方法外,CountDownLatch还提供了带有超时参数的await(long timeout, TimeUnit unit)方法。这个机制允许线程在指定的时间内等待计数器归零。
- 特点:如果超过了指定的时间,线程将不再等待并继续执行后续的任务。这有助于避免无限期地等待某些可能永远不会完成的操作。
== 下图中的T0 T1 T2 T3代表线程名称 ==
CountDownLatch工作过程
初始化计数器:
- 当创建一个 CountDownLatch 实例时,你需要指定一个计数值(count),这个值表示需要等待的事件数或者线程数。
等待操作(await):
- 调用 CountDownLatch 的 await() 方法的线程会被阻塞,直到计数器的值到达零。
- 如果计数器的值已经是零,那么 await() 方法会立即返回,不会造成任何阻塞。
- 可以选择性地调用 await(long timeout, TimeUnit unit),这允许等待一个指定的超时时间,如果在指定的时间内计数器到达零,则 await() 方法返回
true;如果超时时间到达但计数器仍未到零,则返回 false。计数操作(countDown):
- 当一个线程完成了其任务时,它会调用 CountDownLatch 的 countDown() 方法来减少计数器的值。
- 每当 countDown() 被调用时,计数器就会减一。
- 如果计数器的值减到零,那么所有因调用 await() 方法而在等待的线程都会被释放,继续执行。
应用场景
CountDownLatch 常用于控制并发线程的执行顺序。例如,当你有多个线程需要执行一些预备工作,而这些预备工作完成后主线程才能继续执行时,可以使用 CountDownLatch 来实现这种等待机制。
-
任务分解与汇总:当一个大任务需要被分解成多个小任务并行执行,并且主线程需要等待所有小任务完成后才能继续执行时,可以使用CountDownLatch。例如,在搜索引擎中,一个查询请求可能需要被分解成多个子查询并行执行,最后再将结果汇总返回给用户。
-
资源初始化与依赖管理:在应用程序启动阶段或进行某些复杂操作时,可能需要等待多个资源或组件初始化完成后再进行后续操作。通过CountDownLatch,可以确保所有依赖的资源都已经准备好后再继续执行后续的任务。
CountDownLatch实战
示例1:多任务处理的场景(主线程需要等待多个子线程完成各自的任务后才能继续执行)
下面代码使用CountDownLatch模拟了一个多任务处理的场景,其中主线程需要等待多个子线程完成各自的任务后才能继续执行。每个子线程执行一个模拟的任务,例如数据处理或文件下载,并通过countDown()方法通知CountDownLatch任务已完成。主线程则通过await()方法等待所有任务完成。
package com.atguigu.signcenter.nosafe;
import java.util.concurrent.*;
/**
*
* CountDownLatch 通过同步计数器
* @author: jd
* @create: 2024-09-03
*/
public class CountDownLatchExample {
//有6个任务
private static final int TASK_COUNT =6;
// 创建一个线程池来执行任务
static final ThreadPoolExecutor executor = new ThreadPoolExecutor(6,10,30L,
TimeUnit.SECONDS,new LinkedBlockingQueue<Runnable>(),Executors.defaultThreadFactory());
public static void main(String[] args) throws InterruptedException {
// 创建一个CountDownLatch,初始计数值为任务数量
CountDownLatch latch = new CountDownLatch(TASK_COUNT);
//创建线程组,来执行任务
for (int i = 0; i < TASK_COUNT; i++) {
final int taskId = i;
executor.submit(() -> {
//提交每一个子线程 去处理业务
try {
// 模拟任务执行
performTask(taskId);
// 任务完成后,计数器减一
latch.countDown();
} catch (Exception e) {
e.printStackTrace();
}
});
}
//提交完子线程组任务之后,主线程继续往下执行到这里,调用await进行等待(// 主线程等待所有任务完成)
latch.await();
// 关闭线程池(实际使用中应该优雅地关闭线程池)
executor.shutdown();
// 所有任务完成后,主线程继续执行后续操作
System.out.println("所有任务已完成,主线程继续执行...");
}
/**
* 业务操作
* @param taskId
*/
private static void performTask(int taskId) {
System.out.println("任务 " + taskId + " 开始执行...");
try {
// 这里通过睡眠来模拟任务执行耗时
Thread.sleep((long) (Math.random() * 1000));
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("任务 " + taskId + " 执行完成.");
}
}
结果:
任务 0 开始执行...
任务 3 开始执行...
任务 1 开始执行...
任务 2 开始执行...
任务 5 开始执行...
任务 4 开始执行...
任务 1 执行完成.
任务 4 执行完成.
任务 0 执行完成.
任务 2 执行完成.
任务 3 执行完成.
任务 5 执行完成.
所有任务已完成,主线程继续执行...
示例2:一起去爬山
CountDownLatch 实现
小红和小米约定了一起到某个公园,等都到公园了再开始一起爬山
package com.atguigu.signcenter.nosafe;
import java.util.concurrent.*;
/**
*
* 小红小明都到了公园才能一起去爬山
* @author: jd
* @create: 2024-09-03
*/
public class CountDownLatchExampleToMon {
private static final int PEOPLE_COUNT =2;
static final ThreadPoolExecutor executor = new ThreadPoolExecutor(2,5,30L,
TimeUnit.SECONDS,new LinkedBlockingQueue<Runnable>(), Executors.defaultThreadFactory());
public static void main(String[] args) throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(PEOPLE_COUNT);
executor.submit(new Runnable() {
@Override
public void run() {
System.out.println("小明开始出发");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
countDownLatch.countDown(); // 计数器 -1
System.out.println("小明到达人民广场");
}
});
//lambda简化之后写法,直接自动将上面的new Runnable() { 到 public void run() {给去掉了。
executor.submit(() -> {
System.out.println("小红开始出发");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
countDownLatch.countDown(); // 计数器 -1
System.out.println("小红到达人民广场");
});
//主线程等待其他执行完毕后,再继续向下执行
countDownLatch.await();
System.out.println("小明和小红都到达了人民广场,开始一起出发去爬山");
}
}
结果:
小明开始出发
小红开始出发
小明到达人民广场
小红到达人民广场
小明和小红都到达了人民广场,开始一起出发去爬山
通过Join来实现
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(() -> {
System.out.println("小明开始出发");
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("小明到达人民广场");
}, "thread1");
Thread thread2 = new Thread(() -> {
System.out.println("小红开始出发");
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("小红到达人民广场");
}, "thread2");
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println("小明和小红都到达了人民广场,开始一起出发去爬山");
}
结果和上面一样;
使用join()实现和countDownCatch实现好像在代码上的体现并没有太大差异,我们继续往下看
public final void join() throws InterruptedException {
join(0);
}
public final synchronized void join(long millis)
throws InterruptedException {
long base = System.currentTimeMillis();
long now = 0;
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
// 调用join真正执行的方法
if (millis == 0) {
while (isAlive()) {
wait(0);
}
} else {
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break;
}
wait(delay);
now = System.currentTimeMillis() - base;
}
}
}
他的核心代码就几行
while (isAlive()) {
wait(0);
}
通过不停的检查join线程是否存活,如果线程状态是活动的,那么就一直等待下去(wait(0)表示永久等待),直到join线程中止后,线程的this.notifyAll()方法会被调用,不过调用notifyAll()方法是在JVM里 实现的,所以在JDK里看不到
Join()与countDownLatch比较
回到上一个问题,join到底和countDownLatch有什么区别?,countDownLatch底层使用了计数器来控制线程的唤醒,提供了更细粒度的线程控制,比如我们运行了100个线程,但是只需要80个线程执行结束就可以继续下去,那么使用join就不合适了。
综上所述 CountDownLatch相对于Join的优势:
- CountDownLatch可以等待多个线程的完成,而Join只能等待一个线程。
- CountDownLatch可以灵活地设置计数器的值,不仅仅限于线程数,可以根据需要自由控制。
- CountDownLatch提供了更细粒度的线程间协作和控制,可以在任意位置进行countDown()和await()的调用,更灵活地控制线程的流程。
最佳实践
1.异常处理与计数器递减:
在使用CountDownLatch时,应确保子线程在执行任务时能够正确处理异常,并在finally块中调用countDown()方法。这样可以防止因异常导致计数器未能正确减少,从而使主线程永久阻塞在await()方法上。同时,还需要注意不要在countDown()方法调用之前泄露任何可能导致计数器提前归零的操作。
2.避免滥用与性能考虑:
虽然CountDownLatch提供了强大的同步功能,但并不意味着它应该被滥用。在不需要精确同步的场景下,使用其他更简单的同步机制可能更为合适。此外,在高并发场景下,CountDownLatch可能会成为性能瓶颈,因为它需要维护一个计数器并处理多个线程的同步操作。因此,在使用时应充分考虑其对性能的影响,并尝试寻找其他更高效的解决方案。
3.替代方案的选择:
在某些场景下,CyclicBarrier或Semaphore可能是更好的选择。它们提供了与CountDownLatch类似但略有不同的同步机制。例如,CyclicBarrier允许一组线程相互等待直到所有线程都到达某个屏障点后再继续执行;而Semaphore则用于控制对共享资源的访问数量。根据具体需求选择合适的同步工具可以提高代码的效率和可读性。
参考文章:https://blog.csdn.net/qq_26664043/article/details/136636576 、https://www.jb51.net/program/288841ju3.htm