转载

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

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

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型的模块,而且这些模块彼此不影响,每个模块内部的话可能会有共享数据的情况,就需要根据并发的其他知识进行解决了,这里就不多讲了,具体情况具体分析。

原文  https://muyinchen.github.io/2018/05/11/关于CyclicBarrier与CountDownLatch的源码比较-CountDownLatch 使用场景/
正文到此结束
Loading...