Java并发学习(十七)-并发工具CountDownLatch

另一个并发工具,CountDownLatch,和CyclicBarrier功能上有点类似,但是在实现上和是不同的。

What is CountDownLatch

简单的说,也是一个计数器,和CyclicBarrier相比,一定方面是更加灵活的,CountDownLatch可以在任意代码处通知自己已完成,等所有都做完时,主线程就可以接下来运行了。有需要可参看:Java并发学习(十六)-并发工具CyclicBarrier 。

结构

CountDownLatch里面维护这一个AbstractQueuedSynchronizer的子类,其实就是基于AQS的共享锁的应用。
看看Sync的代码:

    private static final class Sync extends AbstractQueuedSynchronizer {
        //count去初始化state。
        Sync(int count) {
            setState(count);
        }

        //获取state。
        int getCount() {
            return getState();
        }

        //重写父类方法,获取共享锁。
        protected int tryAcquireShared(int acquires) {
            return (getState() == 0) ? 1 : -1;
        }

        /**
         * 释放锁。
         * 自减state。
         */
        protected boolean tryReleaseShared(int releases) {
            // Decrement count; signal when transition to zero
            for (;;) {
                int c = getState();
                if (c == 0)
                    return false;
                int nextc = c-1;
                if (compareAndSetState(c, nextc))
                    return nextc == 0;
            }
        }
    }

上述代码只是简单的对AQS的state变量的CAS原子操作。
下面通过主要方法awaitcountDown来分析里面的实现逻辑。

await

一般我们用关于锁的方法时,都是lock和unlock,但是这里是await,你可以理解为,当所有线程执行完后,再需要执行的线程a,那么这个a就可以先await,等到其他的执行完,a就可以执行了。
首先执行await

    public void await() throws InterruptedException {
        sync.acquireSharedInterruptibly(1);
    }

再看父类的acquireSharedInterruptibly方法:

    public final void acquireSharedInterruptibly(int arg)
            throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
            //<0,代表没有获取到锁,所以需要自旋方式再次尝试,
            //结果可能被park挂起,或者获取到锁
        if (tryAcquireShared(arg) < 0)
            doAcquireSharedInterruptibly(arg);
    }

我们再看看子类重写的tryAcquireShared方法:

//重写父类方法,获取共享锁。
        protected int tryAcquireShared(int acquires) {
            return (getState() == 0) ? 1 : -1;
        }

当state为0时候,结果为1,放到acquireSharedInterruptibly 里面就是获取到锁;
而当state不为0,则结果为-1,也就是代表没有获取到锁。

而最开始CountDownLatch的构造方法:

public CountDownLatch(int count) {
        if (count < 0) throw new IllegalArgumentException("count < 0");
        this.sync = new Sync(count);
    }

所以count会传入大于0的,代表等待着count个线程。

现在就明了了,调用这个方法是用来阻塞的。

countDown

countDown什么时候用呢?
上面说过,其他的众多运行线程,countDown方法就是其他众多运行线程用,当你的工作线程运行完后,旧值性countDown方法。

    public void countDown() {
        sync.releaseShared(1);
    }

再看父类的releaseShared方法:

    public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) {
            doReleaseShared();
            return true;
        }
        return false;
    }

tryReleaseShared方法在上面Sync里面,主要就是对count值自减,而doReleaseShared则是unpark唤醒后继符合条件线程。

具体使用例子

从Java doc里面可以看到两个典型例子,这里以例说明。

任务分割情况:

 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));
    //等待,主线程main的await后面代码挂起等待
     doneSignal.await();
     //这后面方法,必须等所有其他的都countDown一次,才会被唤醒执行。          
     afterawait();
   }
 }

 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);
       //我做完了,我利用countDown通知下,但是不阻塞当前线程
       doneSignal.countDown();
     } catch (InterruptedException ex) {} // return;
   }
    //具体工作方法
   void doWork() { ... }
 }

任务分割情况,就是把让其他众多线程a1,a2,a3…..全部运行完后,主线程b才能够执行。注意需要共用这一个CountDownLatch。

多控制计数器:

下面是一对类,其中一组工作线程使用两个倒计时锁存器:

  • 首先是一个启动信号,防止任何工人进行,直到司机准备好进行;
  • 第二个是完成信号,允许驾驶员等待所有工人完成。
 class Driver { // ...
   void main() throws InterruptedException {
   //定义两个CountDownLatch
     CountDownLatch startSignal = new CountDownLatch(1);
     CountDownLatch doneSignal = new CountDownLatch(N);
    //运行一组线程
     for (int i = 0; i < N; ++i) 
       new Thread(new Worker(startSignal, doneSignal)).start();
    //做些事
     doSomethingElse();            
     //做完后,调用startSiganl的countDown
     startSignal.countDown();      
     //做另外一件事
     doSomethingElse();
     //等待doneSignal的信号
     doneSignal.await();          
   }
 }

 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的信号
       startSignal.await();
       //等到信号了,做事
       doWork();
       //对doneSignal发出信号,说做好了
       doneSignal.countDown();
     } catch (InterruptedException ex) {} // return;
   }

   void doWork() { ... }
 }

与CyclicBarrier对比

从功能和实现上,CountDownLatch还是有点区别的:

  • CyclicBarrier内部是使用ReentrantLock和Condition来实现等待和唤醒操作,而CountDownLatch则是直接利用AQS,采用共享锁来实现。
  • CyclicBarrier可以重利用,而CountDownLatch则只能使用一次。
  • CountDownLatch的作用是允许1或N个线程await等待其他线程完成(countDown)执行;而CyclicBarrier则是允许N个线程相互等待,即等到最后一个进入的去唤醒所有等待的。

与join对比

在Thread里面,还有个join方法,用于即阻塞当前线程直到执行join的线程运行完:

public class JoinCountDownLatchTest {
    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(new Runnable() {
            public void run() {
                System.out.println("parser1 finish");
            }
        });
        Thread thread2 = new Thread(new Runnable() {
            public void run() {
                System.out.println("parser2 finish");
            }
        });
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println("all parser finish");
    }
}

运行结果:
Java并发学习(十七)-并发工具CountDownLatch_第1张图片

所以对与join也有等待线程的功能,但是来看join的代码实现:

    public final synchronized void join(long millis)
    throws InterruptedException {
        long base = System.currentTimeMillis();
        long now = 0;
        //判断等待时间
        if (millis < 0) {
            throw new IllegalArgumentException("timeout value is negative");
        }
        //默认等待时间
        if (millis == 0) {
        //如果当前线程仍然存活,那么就一直等待
            while (isAlive()) {
                wait(0);
            }
        } else {
            while (isAlive()) {
                long delay = millis - now;
                if (delay <= 0) {
                    break;
                }
                wait(delay);
                now = System.currentTimeMillis() - base;
            }
        }
    }

最后,直到join线程中止后,线程的this.notifyAll会被调用。所以main线程最后会被唤醒,当然,join方法里面使用了传统的synchronized关键字,以及wait,notify,notifyAll等方法,JDK不推荐在线程实例上使用wait,notify和notifyAll方法。

参考资料:
1. https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/CountDownLatch.html
2. http://ifeve.com/talk-concurrency-countdownlatch/

你可能感兴趣的:(Java并发学习)