首先我们先针对于上一节讲的给出一个很重要的区别:
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 Workerimplements 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 WorkerRunnableimplements 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型的模块,而且这些模块彼此不影响,每个模块内部的话可能会有共享数据的情况,就需要根据并发的其他知识进行解决了,这里就不多讲了,具体情况具体分析。