CountDownLatch:同步多线程的完美助手

188 阅读4分钟

CountDownLatch是Java并发编程中的一种同步工具,它允许一个或多个线程等待其他线程完成操作后再继续执行。它是通过一个计数器来实现的,计数器的初始值为线程的数量。每当一个线程完成了自己的任务后,计数器的值就会减1。当计数器值到达0时,它表示所有的线程已经完成了任务,然后在阻塞等待的线程就可以恢复执行任务。

用一个例子来看下这个用法:

import java.util.concurrent.CountDownLatch;

public class CountDownLatchExample {
  
    public static void main(String[] args) throws InterruptedException {
        int workerCount = 3;
        CountDownLatch latch = new CountDownLatch(workerCount);

        // 创建并启动多个工作线程
        for (int i = 0; i < workerCount; i++) {
            WorkerThread worker = new WorkerThread("Worker " + (i + 1), latch);
            worker.start();
        }

        // 主线程等待所有工作线程完成
        System.out.println("主线程等待工作线程完成...");
        latch.await();

        // 所有工作线程完成后,主线程继续执行
        System.out.println("所有工作线程已完成,主线程继续执行");
    }
}

class WorkerThread extends Thread {
  
    private String name;
    private CountDownLatch latch;

    public WorkerThread(String name, CountDownLatch latch) {
        this.name = name;
        this.latch = latch;
    }

    @Override
    public void run() {
        System.out.println(name + " 开始执行");
        // 模拟工作线程执行任务的耗时
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(name + " 执行完成");
      // 工作线程完成任务,计数器减一
        latch.countDown(); 
    }
}

在上述代码中,首先创建了一个CountDownLatch对象,并设置初始计数器的值为workerCount,这里假设有3个工作线程。

然后,使用一个循环创建并启动了多个WorkerThread工作线程,每个线程执行一些模拟任务。

主线程调用latch.await()方法,使主线程进入等待状态,直到计数器归零。这意味着主线程将等待所有工作线程执行完成。

每个工作线程在执行任务之前,会输出一条开始执行的信息,并模拟任务的耗时。

任务执行完毕后,工作线程调用latch.countDown()方法,将计数器减一。当所有工作线程都完成任务时,计数器归零,主线程解除阻塞,继续执行后续操作。

从代码层面看一下这种机制实现的原理,首先看一下它的构造方法

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

看到this.sync是不是有点熟悉,我在另外一篇信号量:控制并发访问与资源分配中提到过。

Sync(int count) {
   setState(count);
}
protected final void setState(int newState) {
   state = newState;
}

同样的,利用AQS中的state来维护这个计数器。

接着看当工作线程执行完任务后,会将计数器减1。

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

继续点进去,看实现:

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

这段代码已经与Semaphore的release方法执行的是同一段方法入口,只是对应着这几种工具类的不同实现。

在这里我们只需要看CountDownLatch的实现即可。

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;
            }
}

看代码能知道大概干了几件事:

  • 首先,获取当前计数器的值,然后判断计数器的值是否为0。如果为0说明已经没有等待的线程,不需要再执行后面的操作,直接返回false。如果不为0,继续执行,定义一个变量用于接收下一个计数器的值。
  • 然后通过CAS的方式更新计数器的值。compareAndSetState方法是一个原子操作,用于比较当前值和期望值,如果相等则更新为新值,如果不相等则不更新。如果更新成功,说明成功递减计数器的值。
  • 最后,检查递减后的计数器值是否为0。如果为0,表示所有等待的线程都已被释放,可以唤醒所有等待的线程继续执行,此时返回true。如果不为0,表示还有等待的线程,继续等待其他线程释放资源,返回false。

主线程的await():

latch.await();
public void await() throws InterruptedException {
   sync.acquireSharedInterruptibly(1);
}
public final void acquireSharedInterruptibly(int arg)
            throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
        if (tryAcquireShared(arg) < 0)
            doAcquireSharedInterruptibly(arg);
}

和Semaphore中acquire方法的逻辑相同,最终都调了AQS的acquireSharedInterruptibly方法。

总结:

使用CountDownLatch的一般步骤如下:

  1. 创建一个CountDownLatch对象,指定初始计数器的值。

  2. 在主线程中调用await()方法,使主线程等待计数器归零。

  3. 在其他线程中执行任务,任务完成后调用countDown()方法,递减计数器的值。

  4. 当计数器变为零时,主线程解除阻塞,继续执行后续操作。

CountDownLatch的典型应用场景:

  • 主线程等待多个工作线程完成后再继续执行。

  • 并发测试中,主线程等待所有测试线程执行完毕后进行结果统计。

  • 多个线程协同工作,某个线程等待其他线程的信号后再执行特定操作。

总之,CountDownLatch是一种用于线程间协调和同步的工具,它提供了一种简单且灵活的方式,让线程能够等待其他线程的完成。通过合理使用CountDownLatch,可以避免线程之间的竞争条件和数据不一致性问题,实现更加稳定和可靠的并发编程。