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

来源:互联网 发布:java贵美商城 编辑:程序博客网 时间:2024/06/02 02:20

另一个并发工具,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");    }}

运行结果:
这里写图片描述

所以对与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/