CountDownLatch详解-飞
发布时间: 2023-07-06

        CountDownLatch中count down是倒数的意思,latch则是门闩的含义。整体含义可以理解为倒数的门栓,似乎有一点“三二一,芝麻开门”的感觉。CountDownLatch的作用也是如此,在构造CountDownLatch的时候需要传入一个整数n,在这个整数“倒数”到0之前,主线程需要等待在门口,而这个“倒数”过程则是由各个执行线程驱动的,每个线程执行完一个任务“倒数”一次。总结来说,CountDownLatch的作用就是等待其他的线程都执行完任务,必要时可以对各个任务的执行结果进行汇总,然后主线程才继续往下执行。

        CountDownLatch主要有两个方法:countDown()和await()。countDown()方法用于使计数器减一,其一般是执行任务的线程调用,await()方法则使调用该方法的线程处于等待状态,其一般是主线程调用。这里需要注意的是,countDown()方法并没有规定一个线程只能调用一次,当同一个线程调用多次countDown()方法时,每次都会使计数器减一;另外,await()方法也并没有规定只能有一个线程执行该方法,如果多个线程同时执行await()方法,那么这几个线程都将处于等待状态,并且以共享模式享有同一个锁。如下是其使用示例:

public class CountDownLatchExample {  public static void main(String[] args) throws InterruptedException {    CountDownLatch latch = new CountDownLatch(5);    Service service = new Service(latch);    Runnable task = () -> service.exec();    for (int i = 0; i < 5; i++) {      Thread thread = new Thread(task);      thread.start();    }    System.out.println("main thread await. ");    latch.await();    System.out.println("main thread finishes await. ");  }}public class Service {  private CountDownLatch latch;  public Service(CountDownLatch latch) {    this.latch = latch;  }  public void exec() {    try {      System.out.println(Thread.currentThread().getName() + " execute task. ");      sleep(2);      System.out.println(Thread.currentThread().getName() + " finished task. ");    } finally {      latch.countDown();    }  }  private void sleep(int seconds) {    try {      TimeUnit.SECONDS.sleep(seconds);    } catch (InterruptedException e) {      e.printStackTrace();    }  }}

        在上面的例子中,首先声明了一个CountDownLatch对象,并且由主线程创建了5个线程,分别执行任务,在每个任务中,当前线程会休眠2秒。在启动线程之后,主线程调用了CountDownLatch.await()方法,此时,主线程将在此处等待创建的5个线程执行完任务之后才继续往下执行。如下是执行结果:

Thread-0 execute task. Thread-1 execute task. Thread-2 execute task. Thread-3 execute task. Thread-4 execute task. main thread await. Thread-0 finished task. Thread-4 finished task. Thread-3 finished task. Thread-1 finished task. Thread-2 finished task. main thread finishes await. 

        从输出结果可以看出,主线程先启动了五个线程,然后主线程进入等待状态,当这五个线程都执行完任务之后主线程才结束了等待。上述代码中需要注意的是,在执行任务的线程中,使用了try...finally结构,该结构可以保证创建的线程发生异常时CountDownLatch.countDown()方法也会执行,也就保证了主线程不会一直处于等待状态。

        CountDownLatch非常适合于对任务进行拆分,使其并行执行,比如某个任务执行2s,其对数据的请求可以分为五个部分,那么就可以将这个任务拆分为5个子任务,分别交由五个线程执行,执行完成之后再由主线程进行汇总,此时,总的执行时间将决定于执行最慢的任务,平均来看,还是大大减少了总的执行时间。

        另外一种比较合适使用CountDownLatch的地方是使用某些外部链接请求数据的时候,比如图片。在本人所从事的项目中就有类似的情况,因为我们使用的图片服务只提供了获取单个图片的功能,而每次获取图片的时间不等,一般都需要1.5s~2s。当我们需要批量获取图片的时候,比如列表页需要展示一系列的图片,如果使用单个线程顺序获取,那么等待时间将会极长,此时我们就可以使用CountDownLatch对获取图片的操作进行拆分,并行的获取图片,这样也就缩短了总的获取时间。

        CountDownLatch是基于AbstractQueuedSynchronizer实现的,在AbstractQueuedSynchronizer中维护了一个volatile类型的整数state,volatile可以保证多线程环境下该变量的修改对每个线程都可见,并且由于该属性为整型,因而对该变量的修改也是原子的。创建一个CountDownLatch对象时,所传入的整数n就会赋值给state属性,当countDown()方法调用时,该线程就会尝试对state减一,而调用await()方法时,当前线程就会判断state属性是否为0,如果为0,则继续往下执行,如果不为0,则使当前线程进入等待状态,直到某个线程将state属性置为0,其就会唤醒在await()方法中等待的线程。如下是countDown()方法的源代码:

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

        这里sync也即一个继承了AbstractQueuedSynchronizer的类实例,该类是CountDownLatch的一个内部类,其声明如下:

private static final class Sync extends AbstractQueuedSynchronizer {  private static final long serialVersionUID = 4982264981922014374L;  Sync(int count) {    setState(count);  }  int getCount() {    return getState();  }  protected int tryAcquireShared(int acquires) {    return (getState() == 0) ? 1 : -1;  }  protected boolean tryReleaseShared(int releases) {    for (;;) {      int c = getState();   // 获取当前state属性的值      if (c == 0)   // 如果state为0,则说明当前计数器已经计数完成,直接返回        return false;      int nextc = c-1;      if (compareAndSetState(c, nextc)) // 使用CAS算法对state进行设置        return nextc == 0;  // 设置成功后返回当前是否为最后一个设置state的线程    }  }}

        这里tryReleaseShared(int)方法即对state属性进行减一操作的代码。可以看到,CAS也即compare and set的缩写,(苏轼的诗有哪些?苏轼被我们熟知的诗有《题西林壁》、《饮湖上初晴后雨二首·其二》、《水调歌头》、《念奴娇·赤壁怀

微信