想了解CyclicBarrier的底层实现,你可以先去考虑2个问题。
- 一个是需求是什么?
- 第二个是CyclicBarrier可以直接使用AQS来实现吗?
从这个维度出发去思考,能更好的理解CyclicBarrier的底层实现。
同时这个也是一个设计的问题,开发CyclicBarrier的人当时到底是基于什么样的想法来实现的。
在真正开始之前,建议先翻看一下我之前写的关于CyclicBarrier和CountDownLatch 的文章,是一个系列的文章来的,理解之前的这些文章,对阅读本文很有帮助。
好。我们现在开始先说需求:
我们想分批地处理任务,在每一个批次内部,使用多线程并发的方式来提升处理性能。同时,这个分批处理的过程是可循环的,即处理完一批后自动进入下一批,持续执行下去。 这里有一个核心问题:必须保证当前批次内所有并发线程都执行完毕后,才能开始处理下一个批次。也就是说,需要一个同步机制,让所有参与当前批次的线程互相等待,直到最后一个线程也完成,再统一放行进入下一批的处理。
注意这里的描述,有几个关键词:
- 分批,也即是任务按照批次划分;
- 一个分批内必须并发;
- 分批与分批之间必须同步;
- 最后一个是循环;
既然是一个需求,那么各种技术语言里,应该都有对应的实现方案。
比如:
- Java:CyclicBarrier;
- Python:threading.Barrier;
- C++:std::barrier
- 。。。。等等。。。。
既然需求清楚了,我们就来看看Java中CyclicBarrier中的实现方案。
首先想到的是,能不能直接使用AQS呢? 毕竟能复用的就复用,且AQS已经是非常成熟的方案了。
答案是不行的,因为两者的「并发语义」是不太匹配的。
AQS的核心模型是:独占或者共享资源的获取与释放,简单讲就是是一个竞争模型,多个线程争抢有限资源。
而CyclicBarrier如上面需求说的,要应付的是协作相互等待,多个线程到达后,不存在谁抢到谁先执行的问题。
但是我们也不能说直接用Java手写一个,还是可以记住Java里提供的一些基础类的。
那哪些类跟CyclicBarrier的需求是匹配的呢? 就是ReentrantLock + Condition。
因为这两个类精确的对应了CyclicBarrier的核心动作:
- ReentrantLock确保批次内所有任务都完成:因此这里是有计数的动作的,需要线程安全的减一,直到变成0(这里是有判断是否为0的动作的)为止;
- Condition实现批次完成后统一放行:当计数减到0后,需要一次性唤醒所有之前等待的线程;没到齐时,当前线程要挂起等待;
ReentrantLock提供了轻量级的互斥保护,保证同一时刻只有一个线程在修改count并做判断,既安全又高效。
而Condition原生提供await()(挂起)和signalAll()(批量唤醒)。
好,我们用一个表格来完整列一下为啥ReentrantLock + Condition是实现的CyclicBarrier的完美组合。
| CyclicBarrier 的需求 | ReentrantLock + Condition 的能力 | 匹配度 |
|---|---|---|
| 多线程安全修改count | Lock提供互斥临界区 | 完美 |
| 未凑齐时挂起当前线程 | Condition.await() | 完美 |
| 凑齐后批量唤醒所有线程 | Condition.signalAll() | 完美 |
| 支持超时等待 / 中断响应 | Condition.await(timeout) / awaitUninterruptibly | 完美 |
| 每轮结束后重置状态,进入下一轮 | Lock保护下重置count,Condition自动复用 | 完美 |
| 可循环使用 | Lock + Condition 本身无状态,重置count即可复用 | 完美 |
而直接使用AQS来解决这种周期性批量唤醒是很难搞的,或者说,搞不了。
好,下面我留一个思考题:
就是可以使用CAS或者synchronized去替换ReentrantLock?