查看原文
其他

关于CyclicBarrier与CountDownLatch使用场景的探讨...

点击蓝色“Java面试那些事儿”关注我哟
加个“星标”,优质文章,第一时间送达

来源:http://1t.click/apaF


# 前言


首先我们先针对于上一节讲的给出一个很重要的区别:

CountDownLatch 很明显是可以不限制等待线程的数量,而会限制 countDown的操作数。

CyclicBarrier 会限制等待线程的数量。

# 实战


我们来看JDK给我们带来的两种用法:

class Driver { // ... void main() throws InterruptedException { CountDownLatch startSignal = new CountDownLatch(1); CountDownLatch doneSignal = new CountDownLatch(N); for (int i = 0; i < N; ++i) // create and start threads new Thread(new Worker(startSignal, doneSignal)).start(); doSomethingElse(); // don't let run yet <1> startSignal.countDown(); // let all threads proceed <2> doSomethingElse(); // <3> doneSignal.await(); // wait for all to finish <4> }} class Worker implements Runnable { private final CountDownLatch startSignal; private final CountDownLatch doneSignal; Worker(CountDownLatch startSignal, CountDownLatch doneSignal) { this.startSignal = startSignal; this.doneSignal = doneSignal; } public void run() { try { startSignal.await(); // <5> doWork(); doneSignal.countDown(); } catch (InterruptedException ex) {} // return; } void doWork() { ... }}}


这里其实就是在传达信息,首先,这里定义了一个所传状态值为1的 startSignal和状态值为N的 doneSignal,然后通过for循环起了N个线程执行任务,但是在这些线程执行具体任务之前我主线程里有一波逻辑必须先行(因为有些变量的设定是子线程里共享的东西),那么,我就可以在其内进行 startSignal.await()的设定,可以看到,我这里N可以是很大的一个数字,这也就是我们上面讲的 CountDownLatch的一个很强的特性的应用,

接着,在我主线程的一波先行逻辑执行完后(请看<1>),我就可以放行,于是就可以调用<2>处的 startSignal.countDown(),对各个线程进行解除挂起,这里<3>处的代码就和各个子线程里的任务没有什么冲突,也就没什么happen-before这种要求限定了,但我们其他线程就有担心你主线程执行完我任务没完成怎么办,使用sleep?我执行完主线程可能还在等待,这个时间真的不确定,那就在主线程里使用<4>处的代码 doneSignal.await(),

这样,当我各个子线程都结束的时候,我就可以做到主线程在第一时间也可以结束掉省的浪费资源了,这里,有童鞋可能会说主线程里也可以调用XxxThread.join(),但要注意的是,当一个线程调用之后,主线程就休眠了,剩下的join()操作也就无从谈起了,也就是说其他线程结束的时候会调用一下 this.notifyAll但仅针对于这个要结束的线程,所以主线程可能会经历休眠启动,再休眠,再启动,这就浪费性能了。
我们接着看JDK给我们提供的第二个常用使用场景例子:

class Driver2 { // ... void main() throws InterruptedException { CountDownLatch doneSignal = new CountDownLatch(N); Executor e = ...
for (int i = 0; i < N; ++i) // create and start threads e.execute(new WorkerRunnable(doneSignal, i));
doneSignal.await(); // wait for all to finish } }
class WorkerRunnable implements Runnable { private final CountDownLatch doneSignal; private final int i; WorkerRunnable(CountDownLatch doneSignal, int i) { this.doneSignal = doneSignal; this.i = i; } public void run() { try { doWork(i); doneSignal.countDown(); } catch (InterruptedException ex) {} // return; }
void doWork() { ... } }}


这里就实现了一个分治算法应用,首先,我们可以将要做的工作进行策略分割,也就是 doWork()方法实现,里面可以根据所传参数进行策略执行,因为任务要放到线程中执行,而且这里还涉及到了一个策略分配,往往,我们的任务在大局上可以很快的进行策略分块操作,

然后,每一个块内我们可以根据情况假如复杂再进行一个forkJoin的一个应用,这里我们无须去考虑那么多,我们通过实现一个 Runnable来适配Thread需求,

这里,为了适应子线程和主线程的等待执行关系,使用了CountDownLatch来实现,通过上一个例子,大家应该很清楚了,主线程传入一个定义的 CountDownLatch对象,子线程调用,在其 Runnable.run方法的最后调用 doneSignal.countDown()。主线程在其最后调用 doneSignal.await(),这都是固定套路,记住就好。

最后,在 doWork()中根据策略得到的任务很复杂的话,就可以使用 forkJoin策略进行二次分治了,这样就可以做到,分模块,有计算型的模块,也有IO型的模块,而且这些模块彼此不影响,每个模块内部的话可能会有共享数据的情况,就需要根据并发的其他知识进行解决了,这里就不多讲了,具体情况具体分析。

热文推荐
Maven是个什么鬼?如何用?
在IDEA中,如何配置Maven?
一文教会你用IDEA追踪bug(图文版)
同时,分享一份Java面试资料给大家,覆盖了算法题目、常见面试题、JVM、锁、高并发、反射、Spring原理、微服务、Zookeeper、数据库、数据结构等等。


获取方式:点在看,关注公众号并回复 面试 领取。

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存