β

Java 线程同步组件 CountDownLatch 与 CyclicBarrier 原理分析

Harries Blog™ 132 阅读

1.简介

在分析完 AbstractQueuedSynchronizer (以下简称 AQS)和 ReentrantLock 的原理后,本文将分析 java .util.concurrent 包下的两个 线程同步 组件 CountDownLatch CyclicBarrier 。这两个 同步 组件比较常用,也经常被放在一起对比。通过分析这两个同步组件,可使我们对 Java 线程 间协同有更深入的了解。同时通过分析其原理,也可使我们做到知其然,并知其所以然。

这里首先来介绍一下 CountDownLatch 的用途,CountDownLatch 允许一个或一组线程等待其他线程完成后再恢复运行。线程可通过调用 await 方法进入等待状态,在其他线程调用 countDown 方法将计数器减为0后,处于等待状态的线程即可恢复运行。CyclicBarrier (可循环使用的屏障)则与此不同,CyclicBarrier 允许一组线程到达屏障后阻塞住,直到最后一个线程进入到达屏障,所有线程才恢复运行。它们之间主要的区别在于唤醒等待线程的时机。CountDownLatch 是在计数器减为0后,唤醒等待线程。CyclicBarrier 是在计数器(等待线程数)增长到指定数量后,再唤醒等待线程。除此之外,两种之间还有一些其他的差异,这个将会在后面进行说明。

在下一章中,我将会介绍一下两者的实现原理,继续往下看吧。

2.原理

2.1 CountDownLatch 的实现原理

CountDownLatch 的同步功能是基于 AQS 实现的,CountDownLatch 使用 AQS 中的 state 成员变量作为计数器。在 state 不为0的情况下,凡是调用 await 方法的线程将会被阻塞,并被放入 AQS 所维护的同步队列中进行等待。大致示意图如下:

Java 线程同步组件 CountDownLatch 与 CyclicBarrier 原理分析

每个阻塞的线程都会被封装成节点对象,节点之间通过 prev 和 next 指针形成同步队列。初始情况下,队列的头结点是一个虚拟节点。该节点仅是一个占位符,没什么特别的意义。每当有一个线程调用 countDown 方法,就将计数器 state–。当 state 被减至0时,队列中的节点就会按照 FIFO 顺序被唤醒,被阻塞的线程即可恢复运行。

CountDownLatch 本身的原理并不难理解,不过如果大家想深入理解 CountDownLatch 的实现细节,那么需要先去学习一下 AQS 的相关原理。CountDownLatch 是基于 AQS 实现的,所以理解 AQS 是学习 CountDownLatch 的前置条件。我在之前写过一篇关于 AQS 的 文章 Java 重入 ReentrantLock 原理分析 ,有兴趣的朋友可以去读一读。

2.2 CyclicBarrier 的实现原理

与 CountDownLatch 的实现方式不同,CyclicBarrier 并没有直接通过 AQS 实现同步功能,而是在重入锁 ReentrantLock 的基础上实现的。在 CyclicBarrier 中,线程访问 await 方法需先获取锁才能访问。在最后一个线程访问 await 方法前,其他线程进入 await 方法中后,会调用 Condition 的 await 方法进入等待状态。在最后一个线程进入 CyclicBarrier await 方法后,该线程将会调用 Condition 的 signalAll 方法唤醒所有处于等待状态中的线程。同时,最后一个进入 await 的线程还会重置 CyclicBarrier 的状态,使其可以重复使用。

在创建 CyclicBarrier 对象时,需要转入一个值,用于初始化 CyclicBarrier 的成员变量 parties,该成员变量表示屏障拦截的线程数。当到达屏障的线程数小于 parties 时,这些线程都会被阻塞住。当最后一个线程到达屏障后,此前被阻塞的线程才会被唤醒。

3. 源码 分析

通过前面简单的分析,相信大家对 CountDownLatch 和 CyclicBarrier 的原理有一定的了解了。那么接下来趁热打铁,我们一起探索一下这两个同步组件的具体实现吧。

3.1 CountDownLatch 源码分析

CountDownLatch 的原理不是很复杂,所以在具体的实现上,也不是很复杂。当然,前面说过 CountDownLatch 是基于 AQS 实现的,AQS 的实现则要复杂的多。不过这里仅要求大家掌握 AQS 的基本原理,知道它内部维护了一个同步队列,同步队列中的线程会按照 FIFO 依次获取同步状态就行了。好了,下面我们一起去看一下 CountDownLatch 的源码吧。

3.1.1 源码结构

CountDownLatch 的 代码 量不大,加上 注释 也不过300多行,所以它的代码结构也会比较简单。如下:

Java 线程同步组件 CountDownLatch 与 CyclicBarrier 原理分析

如上图,CountDownLatch 源码包含一个 构造方法 和一个私有成员变量,以及数个普通方法和一个重要的静态内部类 Sync。CountDownLatch 的主要逻辑都是封装在 Sync 和其父类 AQS 里的。所以分析 CountDownLatch 的源码, 本质 上是分析 Sync 和 AQS 的原理。相关的分析,将会在下一节中展开,本节先说到这。

3.1.2 构造方法及成员变量

本节来分析一下 CountDownLatch 的构造方法和其 Sync 类型的成员变量实现,如下:

public class CountDownLatch {

    private final Sync sync;

    /** CountDownLatch 的构造方法,该方法要求传入大于0的整型数值作为计数器 */
    public CountDownLatch(int count) {
        if (count < 0) throw new IllegalArgumentException("count < 0");
        // 初始化 Sync
        this.sync = new Sync(count);
    }
    
    /** CountDownLatch 的同步控制器,继承自 AQS */
    private static final class Sync extends AbstractQueuedSynchronizer {
        private static final long serialVersionUID = 4982264981922014374L;

        Sync(int count) {
            // 设置 AQS state
            setState(count);
        }

        int getCount() {
            return getState();
        }

        /** 尝试在共享状态下获取同步状态,该方法在 AQS 中是抽象方法,这里进行了覆写 */
        protected int tryAcquireShared(int acquires) {
            /*
             * 如果 state = 0,则返回1,表明可获取同步状态,
             * 此时线程调用 await 方法时就不会被阻塞。
             */ 
            return (getState() == 0) ? 1 : -1;
        }

        /** 尝试在共享状态下释放同步状态,该方法在 AQS 中也是抽象方法 */
        protected boolean tryReleaseShared(int releases) {
            /*
             * 下面的逻辑是将 state--,state 减至0时,调用 await 等待的线程会被唤醒。
             * 这里使用循环 + CAS,表明会存在竞争的情况,也就是多个线程可能会同时调用 
             * countDown 方法。在 state 不为0的情况下,线程调用 countDown 是必须要完
             * 成 state-- 这个操作。所以这里使用了循环 + CAS,确保 countDown 方法可正
             * 常运行。
             */
            for (;;) {
                // 获取 state
                int c = getState();
                if (c == 0)
                    return false;
                int nextc = c-1;
                
                // 使用 CAS 设置新的 state 值
                if (compareAndSetState(c, nextc))
                    return nextc == 0;
            }
        }
    }
}
作者:Harries Blog™
追心中的海,逐世界的梦

发表评论