CountDownLatch实现原理全面解析

简介

CountDownLatch是一个同步工具类,用来协调多个线程之间的同步(即:用于线程之间的通信而不是互斥)。它允许一个或多个线程进入等待状态,直到其他线程执行完毕后,这些等待的线程才继续执行。

CountDownLatch通过一个计数器来实现,其中维护了一个count变量和操作该变量的两个主要方法:

  • await()方法:线程调用await()方法,会使调用该方法的线程进入阻塞状态,并将其加入到阻塞队列中。
  • countDown()方法:线程调用countDown()方法,会将CountDownLatch中count的值-1。当count变量的值递减为0,会唤醒阻塞队列中调用await()方法的线程继续执行业务处理。

应用场景

CountDownLatch是一种非常实用的并发控制工具,它的主要应用场景:

  • 主线程等待多个子线程完成任务处理。如:主线程等待其他线程各自完成任务处理后,再继续执行。

  • 实现多个线程开始执行任务处理的最大并行性(注意:是并行而非并发)。如:多个线程需要在同一时刻开始执行任务处理,可以通过如下方式实现:

    1)初始化一个的CountDownLatch变量(计数器的初始化值为1)。

    2)需要在同一时刻执行任务处理的所有线程调用CountDownLatch.await()方法进入阻塞状态。

    3)主线程调用CountDownLatch.countDown()方法将计数器-1(此时计数器的值为0),唤醒所有调用CountDownLatch.await()方法进入阻塞状态的线程开始执行任务处理。

实现原理

CountDownLatch中定义了一个Sync类型的变量和操作该变量的方法。

源码如下:

// Sync类型的同步变量
private final Sync sync;
// 构造函数,用于初始化CountDownLatch计数器
public CountDownLatch(int count) {...}
// 当前线程进入阻塞状态,直到AQS中的state(计数器)值为0,或者当前线程被其他线程中断。
public void await() throws InterruptedException {...}
// 当前线程进入阻塞状态,直到AQS中的state(计数器)值为0,或者当前线程等待超时或者被其他线程中断。
public boolean await(long timeout, TimeUnit unit) throws InterruptedException {...}
// 递减AQS中的state(计数器)值,如果state的值递减为0,则唤醒调用await()方法进入阻塞的线程。
public void countDown() {...}
// 返回state的值。
public long getCount() {...}
// 返回标识CountDownLatch及其计数器值的字符串。
public String toString() {...}

其中,最重要的是sync类型的变量、await()和countDown()方法。

Sync

Sync是CountDownLatch的静态内部类器,它继承了 AbstractQueuedSynchronizer(AQS),主要用于CountDownLatch的同步状态,创建CountDownLatch时进行初始化。

CountDownLatch构造函数:

public CountDownLatch(int count) {
    // 如果传入的count值小于0,则抛出IllegalArgumentException异常
    if (count < 0throw new IllegalArgumentException("count < 0");
    // 初始化Sync
    this.sync = new Sync(count);
}

初始化Sync时,将传入的count参数值赋值给AQS的同步状态state,state是一个volatile修饰的int值,一个线程修改了state值,其他线程能够立刻感知,从而保证state值在并发场景下的可见性。

同时,Sync实现了AQS的tryAcquireShared()和tryReleaseShared()方法:

java.util.concurrent.CountDownLatch.Sync#tryAcquireShared
// 尝试获取共享资源
protected int tryAcquireShared(int acquires) {
    /**
     * 用于根据state(计数器)的值来尝试获取共享资源:
     *   state的值为0,返回1,表示可以获取共享资源。
     *   state的值不为0,返回-1,表示无法获取共享资源。
     */

    return (getState() == 0) ? 1 : -1;
}
// 尝试释放共享资源(AQS)
protected boolean tryReleaseShared(int releases) {
    // Decrement count; signal when transition to zero
    for (;;) {
        // 获取当前state的值
        int c = getState();
        // 如果state的值为0,则返回false(即:没有需要释放的资源)
        if (c == 0)
            return false;
        // 如果state的值大于0,则将state的值-1,并通过CAS的方式更新state的最新值
        int nextc = c-1;
        if (compareAndSetState(c, nextc))
            /** 
             * 返回资源释放结果:
             *   释放资源后state的值为0,则返回true,表示可以唤醒调用await()方法进入阻塞的线程。
             *   释放资源后state的值不为0,则返回false,表示继续阻塞调用await()方法的线程,直到state的值被减为0。
             */

            return nextc == 0;
    }
}

await方法

CountDownLatch通过CountDownLatch#await方法调用CountDownLatch.Sync#tryAcquireShared方法尝试获取共享资源:

  • 获取到共享资源,则唤醒调用await()方法的线程执行业务处理。
  • 获取不到共享资源,则继续阻塞调用await()方法的线程,直到state的值递减为0(即:其他线程释放完共享资源)。

CountDownLatch#await方法源码解析:

// java.util.concurrent.CountDownLatch#await()
public void await() throws InterruptedException {
    // 获取共享资源(可中断)
    sync.acquireSharedInterruptibly(1);
}
// java.util.concurrent.locks.AbstractQueuedSynchronizer#acquireSharedInterruptibly
// 获取共享资源(AQS)
public final void acquireSharedInterruptibly(int arg)
        throws InterruptedException 
{
    // 如果线程被其他线程中断,则抛出InterruptedException异常
    if (Thread.interrupted())
        throw new InterruptedException();
    // 具体由继承AQS的Sync的tryAcquireShared()方法实现
    if (tryAcquireShared(arg) < 0)
        // 如果获取共享资源锁失败,则将当前线程封装成Node节点追加到CLH队列的末尾,等待被唤醒(即:进入阻塞)
        doAcquireSharedInterruptibly(arg);
}

countDown方法

CountDownLatch通过CountDownLatch#countDown方法调用CountDownLatch.Sync#tryReleaseShared方法尝试释放共享资源:

  • 如果释放某个共享资源后state的值为0,则唤醒调用await()方法的线程执行业务处理。
  • 如果释放某个共享资源后state的值不为0,则继续阻塞调用await()方法的线程,直到state的值被减为0。

CountDownLatch#countDown方法源码解析:

// java.util.concurrent.CountDownLatch#countDown
public void countDown() {
    // 释放共享资源(数量为1)
    sync.releaseShared(1);
}
// java.util.concurrent.locks.AbstractQueuedSynchronizer#releaseShared
// 释放共享资源(AQS)
public final boolean releaseShared(int arg) {
    // 具体由继承AQS的Sync的tryReleaseShared()方法实现
    if (tryReleaseShared(arg)) {
        // 唤醒后继节点
        doReleaseShared();
        return true;
    }
    return false;
}

使用示例

主线程等待子线程完成处理

/**
 * @author 南秋同学
 * 主线程等待多个子线程完成任务处理
 */

@Slf4j
public class CountDownLatchExample {
    @SneakyThrows
    public static void main(String[] args) {
        // 初始化一个的CountDownLatch变量(计数器的初始化值为5)
        CountDownLatch cdl = new CountDownLatch(5);
        // 初始化一个固定大小的线程池
        ExecutorService service = Executors.newFixedThreadPool(5);
        for(int i = 0; i < 5 ; i++){
            // 创建Runnable线程
            Runnable runnable = new Runnable() {
                @SneakyThrows
                @Override
                public void run() {
                    log.info("子线程-{}开始执行...", Thread.currentThread().getName());
                    Thread.sleep((long) (Math.random() * 10000));
                    log.info("子线程-{}执行完成", Thread.currentThread().getName());
                    cdl.countDown();
                }
            };
            service.execute(runnable);
        }
        log.info("主线程-{}等待所有子线程执行完成...",Thread.currentThread().getName());
        cdl.await();
        log.info("所有子线程执行完成,开始执行主线程-{}",Thread.currentThread().getName());
    }
}

执行结果:

14:13:01.299 [pool-1-thread-3]  - 子线程-pool-1-thread-3开始执行...
14:13:01.299 [pool-1-thread-2] - 子线程-pool-1-thread-2开始执行...
14:13:01.299 [main] - 主线程-main等待所有子线程执行完成...
14:13:01.299 [pool-1-thread-1] - 子线程-pool-1-thread-1开始执行...
14:13:01.299 [pool-1-thread-5] - 子线程-pool-1-thread-5开始执行...
14:13:01.299 [pool-1-thread-4] - 子线程-pool-1-thread-4开始执行...
14:13:02.739 [pool-1-thread-3] - 子线程-pool-1-thread-3执行完成
14:13:03.792 [pool-1-thread-1] - 子线程-pool-1-thread-1执行完成
14:13:04.752 [pool-1-thread-5] - 子线程-pool-1-thread-5执行完成
14:13:07.761 [pool-1-thread-4] - 子线程-pool-1-thread-4执行完成
14:13:10.384 [pool-1-thread-2] - 子线程-pool-1-thread-2执行完成
14:13:10.385 [main] - 所有子线程执行完成,开始执行主线程-main

多线程最大并行处理

/**
 * @author 南秋同学
 * 实现多个线程开始执行任务处理的最大并行性
 */

@Slf4j
public class CountDownLatchExample {
    @SneakyThrows
    public static void main(String[] args) {
        // 初始化一个的CountDownLatch变量(计数器的初始化值为1)
        CountDownLatch referee = new CountDownLatch(1);
        // 初始化一个的CountDownLatch变量(计数器的初始化值为5)
        CountDownLatch sportsman = new CountDownLatch(5);
        // 初始化一个固定大小的线程池
        ExecutorService service = Executors.newFixedThreadPool(5);
        for(int i = 0; i < 5 ; i++){
            // 创建Runnable线程
            Runnable runnable = new Runnable() {
                @SneakyThrows
                @Override
                public void run() {
                    log.info("运动员-{},等待裁判发布开始口令", Thread.currentThread().getName());
                    referee.await();
                    log.info("运动员-{},收到裁判发布的开始口令,起跑...", Thread.currentThread().getName());
                    Thread.sleep((long) (Math.random() * 10000));
                    log.info("运动员-{}到达终点", Thread.currentThread().getName());
                    sportsman.countDown();
                }
            };
            service.execute(runnable);
        }
        log.info("裁判-{}准备发布开始口令...",Thread.currentThread().getName());
        Thread.sleep((long) (Math.random() * 10000));
        referee.countDown();
        log.info("裁判-{}已经发布开始口令,等待所有选手达到终点...",Thread.currentThread().getName());
        sportsman.await();
        log.info("所有运动员达到终点,裁判-{}开始计分",Thread.currentThread().getName());
    }
}

执行结果:

13:56:14.683 [pool-1-thread-3]  - 运动员-pool-1-thread-3,等待裁判发布开始口令
13:56:14.683 [pool-1-thread-2]  - 运动员-pool-1-thread-2,等待裁判发布开始口令
13:56:14.683 [pool-1-thread-5]  - 运动员-pool-1-thread-5,等待裁判发布开始口令
13:56:14.683 [main]  - 裁判-main准备发布开始口令...
13:56:14.683 [pool-1-thread-1]  - 运动员-pool-1-thread-1,等待裁判发布开始口令
13:56:14.683 [pool-1-thread-4]  - 运动员-pool-1-thread-4,等待裁判发布开始口令
13:56:18.205 [main]  - 裁判-main已经发布开始口令,等待所有选手达到终点...
13:56:18.205 [pool-1-thread-2]  - 运动员-pool-1-thread-2,收到裁判发布的开始口令,起跑...
13:56:18.205 [pool-1-thread-3]  - 运动员-pool-1-thread-3,收到裁判发布的开始口令,起跑...
13:56:18.206 [pool-1-thread-5]  - 运动员-pool-1-thread-5,收到裁判发布的开始口令,起跑...
13:56:18.206 [pool-1-thread-4]  - 运动员-pool-1-thread-4,收到裁判发布的开始口令,起跑...
13:56:18.206 [pool-1-thread-1]  - 运动员-pool-1-thread-1,收到裁判发布的开始口令,起跑...
13:56:22.110 [pool-1-thread-4]  - 运动员-pool-1-thread-4到达终点
13:56:23.866 [pool-1-thread-1]  - 运动员-pool-1-thread-1到达终点
13:56:26.803 [pool-1-thread-3]  - 运动员-pool-1-thread-3到达终点
13:56:28.019 [pool-1-thread-5]  - 运动员-pool-1-thread-5到达终点
13:56:28.178 [pool-1-thread-2]  - 运动员-pool-1-thread-2到达终点
13:56:28.179 [main]  - 所有运动员达到终点,裁判-main开始计分


CountDownLatch实现原理全面解析


原文始发于微信公众号(小小开发者):CountDownLatch实现原理全面解析

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/230984.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!