- 作者:老汪软件技巧
- 发表时间:2024-10-06 15:01
- 浏览量:
首页加载太慢,需要几秒,有一个聚合等待逻辑比较耗时。其中有一个数据需要其他几个数据计算出来以后,才能计算。
系统运行一段时间,开始变得慢起来, 检查发现请求接口串行。
使用 CountDownLatch 将依赖的数据并行计算。
1.4 故事四:更新丢失
写出数据,依赖前面的更新数据。由于更新数据较多,采用多线程,但是并没等每个任务都更新结束。因此存在部分数据不符合预期。(多线程执行更新,但是没有等待执行结果就进入下一步)
使用 CountDownLatch,等所有数据更新结束,再读取。显然这是非常典型的依赖。
故事最后:我们没有批评和抨击别人的权利,但是我们要有解决问题的实际能力!
二、依赖等待是本质
CountDownLatch 属于线程之间的通信,或者线程之间的协作,但我认为它解决的是多任务依赖等待问题。
2.1 案例描述
如下图所示: ABC 都是繁重的 http/RPC 接口,执行完毕才能执行 D,因此整个执行时间,随着 ABC 耗时增加而增加。但是并行任务,则取 ABC 的最大值即可。因此在耗时上来讲,串行属于时间和,并行属于时间极值。
2.2 场景总结
那么哪些场景可以使用 CountDownLatch。 简言之:有多任务执行结果的依赖都可以考虑 CountDownLatch
三、认识 CountDownLatch
A synchronization aid that allows one or more threads to wait until a set of operations being performed in other threads completes(一个同步辅助工具,允许一个或多个线程等待其他线程执行的一组操作完成)
CountDownLatch 初始化一个给定值,调用 await 方法阻塞,直到当前计数由于调用 countDown 方法而达到零,
之后所有等待的线程都会被释放。
3.1 CountDownLatch 接口
CountDownLatch 源码给出了两个使用案例。(只是简单 demo,实践在使用上我们更应该考虑异常)
CountDownLatch 的关键方法:
3.2 注意事项
注意事项: countDownLatch 的数量减到 0, 否则会存在一直等待,因为其他线程没有办法继续获取锁。这也是十分危险的事情。
无法执行 countDown(), 导致其他等待线程无法执行。例异常时,不能执行到该段代码一样。
可以将await() 替换成await(long timeout, TimeUnit unit) 带有超时机制的等待方法,避免特殊情况的等待。
3.3 替代 CompletableFuture 工具类
可以使用 CompletableFuture 这些 API 来替换 CountDownLatch 的功能。 CompletableFuture 提供了较多的 API 来替代 CountDownLatch。 处理异常时特别方便的。
当然 CompletableFuture 还有很多高级 API,可以根据场景选择合适的 API。 虽然 CompletableFuture 很好用,但是我也建议你尝试用用 CountDownLatch。
3.4 CountDownLatch 部分源码
CountDownLatch 的代码很少,主要复用了 AbstractQueuedSynchronizer (AQS)的能力。
CountDownLatch 的内部类 Sync 继承自 AbstractQueuedSynchronizer,重写了 tryAcquireShared 和 tryReleaseShared 方法来实现同步逻辑。tryAcquireShared 方法用于判断是否可以获取共享锁(即计数器是否为零),而 tryReleaseShared 方法用于减少计数器并唤醒等待的线程。
private static final class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = 4982264981922014374L;
Sync(int count) {
setState(count);
}
int getCount() {
return getState();
}
// ===== 实现 tryAcquireShared 和 tryReleaseShared 两个方法 ===
// 这个方法是用来尝试获取共享模式下的同步状态。
// 在 CountDownLatch 的使用场景中,当一个或多个线程调用 await() 方法时,
// 它们会尝试获取共享锁。tryAcquireShared 方法的实现逻辑是检查 AQS 同步状态
//(即 CountDownLatch 的计数器)是否为0
protected int tryAcquireShared(int acquires) {
return (getState() == 0) ? 1 : -1;
}
// 这个方法是用来尝试释放共享模式下的同步状态。
// 当线程调用 countDown() 方法时,实际上是在减少 CountDownLatch 的计数器
/*
1. 通过循环,获取当前同步状态(计数器值)。
2. 如果计数器已经为0,说明已经没有需要等待的事件了,方法返回false,不再执行任何操作。
3. 如果计数器不为0,方法会尝试通过 CAS(比较并交换)操作将计数器减1。
4. 如果 CAS 操作成功,并且新的计数器值为0,表示所有事件都已经完成,方法返回true,此时 AQS 会释放所有等待的线程。
*/
protected boolean tryReleaseShared(int releases) {
// Decrement count; signal when transition to zero
for (;;) {
int c = getState();
if (c == 0)
return false;
int nextc = c-1;
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}
}
为什么实现这两个方法
tryAcquireShared 和 tryReleaseShared 是为了定义 CountDownLatch 特定的同步逻辑:
tryAcquireShared: 确定调用 await() 的线程是否应该被阻塞。如果是在计数器还没减到 0 的时候调用,那么线程应该被阻塞。tryReleaseShared: 定义当一个事件完成时(调用 countDown()),计数器如何减少,并在所有事件都完成时(计数器为0)释放所有等待的线程。
这两个方法是 AQS 框架中的钩子方法,允许开发者定义自己的同步逻辑,以实现不同的同步工具,如 CountDownLatch、Semaphore、ReentrantLock 等。通过这种方式,CountDownLatch 能够提供一种机制,让一个或多个线程等待直到其他线程执行完毕某些操作。
四、结束语4.1 面试经历
曾经被大厂的面试官问过是否使用过 CountDownLatch,当然也当过面试官考核过别人。(历史总是这样惊人地相似)
考核的背后原因:
校招同学:是否知道 CountDownLatch 这个知识点,有没有进一步了解原理。 以考核知识点,技术钻研度为主。
社招同学:有没有使用经验,能不能遇到 CountDownLatch 场景能够合理正确地使用。以考核实际应用为主。
特别说明:面试并不是说让你背下多少源码,也并不需要这么做。更应该理解源码后,梳理出原理、流程。 不可本末倒置,以为理解源码就是背诵和记忆源码,并不是。
4.2 CountDownLatch 总结
如何介绍 CountDownLatch,可以从功能理解、应用场景、使用经验和注意事项
功能介绍: Java 中的一个同步辅助类,它允许一个或多个线程等待一组操作完成; 线程之间的协作!
场景经验: 前后依赖任务,前面属于多任务,可并行执行。 比如部门人员同步、数据聚合等待、异步数据获取返回。注意需要保证 countDown() 方法的调用,特别注意在异常情况
部分源码:使用上的两个核心方法:countDown() 和 await()/await(...); 内部继承 AbstractQueuedSynchronizer (AQS) 作为其同步器。并继承重写 tryAcquireShared/tryReleaseShared 作为判断获取公平锁和释放锁的关键
4.3 一些感想
以前觉得这个 countDownLatch 没有太大的作用,直到在一些场景后实践后才知道它是有多么的好用,但是也有很多注意事项,要非常谨慎地处理。(在使用任何线程工具的时候,都需要记住异常、边界问题的处理)
每一次换工作都免不了要做一些重复的事情,希望这是最后一次!毕竟重复的事情是浪费生命!
从上一家离职后所有资料都回收了,可惜好多代码和文档都不能拿走,能带走就只有思路了。
好了,本文到此结束。