关于CyclicBarrier与CountDownLatch的源码比较-CountDownLatch 使用场景

关于CyclicBarrier与CountDownLatch的比较与使用场景的一些讨论

前言

首先我们先针对于上一节讲的给出一个很重要的区别:
CountDownLatch 很明显是可以不限制等待线程的数量,而会限制 countDown的操作数。
CyclicBarrier 会限制等待线程的数量。

实战

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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给我们提供的第二个常用使用场景例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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型的模块,而且这些模块彼此不影响,每个模块内部的话可能会有共享数据的情况,就需要根据并发的其他知识进行解决了,这里就不多讲了,具体情况具体分析。

本文配套分享视频:

http://v.youku.com/v_show/id_XMzYwMDE3ODA3Ng==.html?spm=a2h3j.8428770.3416059.1

您的支持将鼓励我继续创作!