并发编程原理与实战(六)详解并发协同利器CountDownLatch

并发编程原理与实战(一)精准理解线程的创建和停止

并发编程原理与实战(二)Thread类关键API详解

并发编程原理与实战(三)一步步剖析线程返回值

并发编程原理与实战(四)经典并发协同方式synchronized与wait+notify详解

并发编程原理与实战(五)经典并发协同方式伪唤醒与加锁失效原理揭秘

前面分析了通过synchronized和wait(),notify(),notifyAll()方法实现传统多线程并发协同的方式,并通过“两个线程顺序交替输出数字”,“生产者-消费者模型处理任务”等例子来说明。这两个例子都是基于无限循环条件判断,满足条件的线程就进入运行状态,不满足条件就进入等待状态,运行完成的线程触发修改条件并唤醒等待状态的线程执行,这是多线程协同运行的场景之一。

现在我们有个场景:有一个订单查询详情接口,执行的业务逻辑比较多,先查询订单主要信息,再查订单商品信息,接着查询卖家信息,再查订单买家信息,最后将这些信息返回给前端。要查这么多信息,如果用单个线程串行执行,耗时比较久;现在要用多线程去改造,每部分信息用一个子线程去查询,这样每部分信息的查询就可以并行执行,最后主线程负责将查询结果返回前端。相比单个主线程串行执行,多个子线程并行查询会不会加快整个接口的响应速度,接下来论证下。

先分析下这个场景,用synchronized和wait(),notify(),notifyAll()方法能否实现?这个场景中,主线程负责接收前端请求、创建子线程执行信息查询、返回查询结果到前端;子线程负责查询指定信息。主子线程运行的条件都是一样,就是有前端请求过来,请求过来后需要主线程等待子线程执行完成,子线程并不需要等待主线程,子线程之间也不需要相互等待或者唤醒。很明显,用传统并发协同方法无法实现这样的线程协同场景,因为这里不需要无限循环判断条件是否满足,不需要线程之间的相互唤醒。那么jdk中还提供了哪些并发协同工具可以用于这种场景?倒计时锁存器是其中一种。

倒计时锁存器CountDownLatch

CountDownLatch 是 Java 并发包(java.util.concurrent)中一个重要的线程并发协同工具类,用于‌控制多个线程的执行顺序‌,它允许一个或多个线程等待其他线程完成一系列操作后再继续执行,这个就适合用于对订单详情查询接口的改造。下面看看官方说明。

/**
 * A synchronization aid that allows one or more threads to wait until
 * a set of operations being performed in other threads completes.
 *
 * <p>A {@code CountDownLatch} is initialized with a given <em>count</em>.
 * The {@link #await await} methods block until the current count reaches
 * zero due to invocations of the {@link #countDown} method, after which
 * all waiting threads are released and any subsequent invocations of
 * {@link #await await} return immediately.  This is a one-shot phenomenon
 * -- the count cannot be reset.  If you need a version that resets the
 * count, consider using a {@link CyclicBarrier}.
 *
 * <p>A {@code CountDownLatch} is a versatile synchronization tool
 * and can be used for a number of purposes.  A
 * {@code CountDownLatch} initialized with a count of one serves as a
 * simple on/off latch, or gate: all threads invoking {@link #await await}
 * wait at the gate until it is opened by a thread invoking {@link
 * #countDown}.  A {@code CountDownLatch} initialized to <em>N</em>
 * can be used to make one thread wait until <em>N</em> threads have
 * completed some action, or some action has been completed N times.
 *
 * <p>A useful property of a {@code CountDownLatch} is that it
 * doesn't require that threads calling {@code countDown} wait for
 * the count to reach zero before proceeding, it simply prevents any
 * thread from proceeding past an {@link #await await} until all
 * threads could pass.
 *
 ....
 *
 * <p>Memory consistency effects: Until the count reaches
 * zero, actions in a thread prior to calling
 * {@code countDown()}
 * <a href="package-summary.html#MemoryVisibility"><i>happen-before</i></a>
 * actions following a successful return from a corresponding
 * {@code await()} in another thread.
 *
 * @since 1.5
 * @author Doug Lea
 */
public class CountDownLatch {
...
}

通过给定一个数字初始化CountDownLatch对象,或者叫初始化倒计时锁存器,线程调用倒计时锁存器的 countDown() 方法减少计数,线程调用倒计时锁存器的await()方法会阻塞,直到计数减到零才会继续运行。这个倒计时锁存器就像一个门闩,没有减到零时,所有调用了await()方法的线程都集中门里等待,只有减到零后才会开门让线程继续执行。

这里需要注意的是,调用 countDown() 方法的线程在运行前并不需要等待计数减到零。这和上面分析的订单详情查询接口中,子线程不需要等待主线程场景吻合。

倒计时锁存器方法分析

倒计时锁存器的成员方法并不多,我们来逐一分析下。

1、构造函数CountDownLatch(int count)

/**
 * Constructs a {@code CountDownLatch} initialized with the given count.
 *
 * @param count the number of times {@link #countDown} must be invoked
 *        before threads can pass through {@link #await}
 * @throws IllegalArgumentException if {@code count} is negative
 */
public CountDownLatch(int count) {
    if (count < 0) throw new IllegalArgumentException("count < 0");
    this.sync = new Sync(count);
}

该构造函数通过指定一个整型值创建一个CountDownLatch对象,这个整型值就是countDown() 方法被调用的次数。

2、countDown()方法

/**
 * Decrements the count of the latch, releasing all waiting threads if
 * the count reaches zero.
 *
 * <p>If the current count is greater than zero then it is decremented.
 * If the new count is zero then all waiting threads are re-enabled for
 * thread scheduling purposes.
 *
 * <p>If the current count equals zero then nothing happens.
 */
public void countDown() {
    sync.releaseShared(1);
}

调用该方法将会对计数器减1,如果减到0将会释放所有等待状态的线程,这些线程将会被调度运行。如果当前计数器等于0,调用该方法将什么都不做。

3、await()方法

/**
 * Causes the current thread to wait until the latch has counted down to
 * zero, unless the thread is {@linkplain Thread#interrupt interrupted}.
 *
 * <p>If the current count is zero then this method returns immediately.
 *
 * <p>If the current count is greater than zero then the current
 * thread becomes disabled for thread scheduling purposes and lies
 * dormant until one of two things happen:
 * <ul>
 * <li>The count reaches zero due to invocations of the
 * {@link #countDown} method; or
 * <li>Some other thread {@linkplain Thread#interrupt interrupts}
 * the current thread.
 * </ul>
 *
 * <p>If the current thread:
 * <ul>
 * <li>has its interrupted status set on entry to this method; or
 * <li>is {@linkplain Thread#interrupt interrupted} while waiting,
 * </ul>
 * then {@link InterruptedException} is thrown and the current thread's
 * interrupted status is cleared.
 *
 * @throws InterruptedException if the current thread is interrupted
 *         while waiting
 */
public void await() throws InterruptedException {
    sync.acquireSharedInterruptibly(1);
}

调用该方法将会导致线程进入等待状态直到计数器减到0,除非线程被中断等待。如果计数器等于0,调用该方法会立即返回。

4、boolean await(long timeout, TimeUnit unit)方法

/**
 * Causes the current thread to wait until the latch has counted down to
 * zero, unless the thread is {@linkplain Thread#interrupt interrupted},
 * or the specified waiting time elapses.
 *
...
 */
public boolean await(long timeout, TimeUnit unit)
    throws InterruptedException {
    return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
}

该方法和上一个方法的区别是可以指定等待时间和有返回值。调用该方法将会导致线程进入等待状态直到计数器减到0,除非线程被中断等待或者指定的等待时间用完。如果计数器已经减到0了,那么将返回true;如果在等待时间用完前计数器还没有减到0那么将返false。

5、long getCount()方法

/**
 * Returns the current count.
 *
 * <p>This method is typically used for debugging and testing purposes.
 *
 * @return the current count
 */
public long getCount() {
    return sync.getCount();
}

返回当前计数器的数值,通常用于调试和测试。

应用举例

分析完CountDownLatch的构造函数和成员方法后,我们把这个倒计时锁存器应用到订单详情查询接口改造中,相应的就是主线程调用await()方法等待子线程执行完成,子线程完成查询后调用countDown() 方法对计数减1。

public class CountDownLatchDemo {
    public static void main(String[] args) throws Exception{
        long start = System.currentTimeMillis();
        ConcurrentHashMap<String,String> retMap = new ConcurrentHashMap();
        CountDownLatch countDownLatch = new CountDownLatch(4);
        //订单查询
        new Thread(()->{
            System.out.println(Thread.currentThread().getName()+"订单查询开始...");
            //模拟查询耗时
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            retMap.put("orderInfo","7897562412");
            countDownLatch.countDown();
            System.out.println(Thread.currentThread().getName()+"订单查询完成...");
        },"t1").start();

        //商品查询
        new Thread(()->{
            System.out.println(Thread.currentThread().getName()+"商品查询开始...");
            //模拟查询耗时
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            retMap.put("goodsInfo","香蕉");
            countDownLatch.countDown();
            System.out.println(Thread.currentThread().getName()+"商品查询完成...");
        },"t2").start();

        //卖家信息查询
        new Thread(()->{
            System.out.println(Thread.currentThread().getName()+"卖家信息查询开始...");
            //模拟查询耗时
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            retMap.put("storeInfo","鲜果铺");
            countDownLatch.countDown();
            System.out.println(Thread.currentThread().getName()+"卖家信息查询完成...");
        },"t3").start();

        //买家信息查询
        new Thread(()->{
            System.out.println(Thread.currentThread().getName()+"买家信息查询开始...");
            //模拟查询耗时
            try {
                Thread.sleep(600);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            retMap.put("buyerInfo","帧栈");
            countDownLatch.countDown();
            System.out.println(Thread.currentThread().getName()+"买家信息查询完成...");
        },"t4").start();

        System.out.println("主线程等待中...");
        countDownLatch.await();

        System.out.println("订单详情查询完成,"+ JSON.toJSONString(retMap));
        long end = System.currentTimeMillis();
        System.out.println("耗时"+ (end-start)+"毫秒");
    }
}

运行结果:

主线程等待中...
t1订单查询开始...
t2商品查询开始...
t3卖家信息查询开始...
t4买家信息查询开始...
t3卖家信息查询完成...
t4买家信息查询完成...
t2商品查询完成...
t1订单查询完成...
订单详情查询完成,{"buyerInfo":"帧栈","orderInfo":"7897562412","storeInfo":"鲜果铺","goodsInfo":"香蕉"}
耗时2124毫秒

如果是单线程串行执行,那么总耗时至少大于等于各个查询的耗时之和4100(2000+1000+500+600)毫秒,从运行结果可以看出,改成并行后整个查询耗时缩短到2124毫秒。

另一个常见的场景是多个线程需要等待统一的指令后同时执行,比如模拟高并发测试,多个线程同时对一个接口发起调用。

public class ConcurrentStartDemo {
    public static void main(String[] args) throws InterruptedException {
        int threads = 5;
        // 开始信号
        CountDownLatch startSignal = new CountDownLatch(1);
        // 准备完成信号
        CountDownLatch readySignal = new CountDownLatch(threads);
        // 调用完成信号
        CountDownLatch doneSignal = new CountDownLatch(threads);
        for (int i = 0; i < threads; i++) {
            new Thread(() -> {
                try {
                    //准备完成
                    readySignal.countDown();
                    System.out.println(Thread.currentThread().getName() + "," + " 已准备就绪,等待开始信号...");
                    // 等待主线程发出开始信号
                    startSignal.await();
                    System.out.println("时间戳" + System.currentTimeMillis() + "," + Thread.currentThread().getName() + " 开始调用接口...");
                    // 模拟调用耗时
                    Thread.sleep(1000);
                    doneSignal.countDown();

                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }).start();
        }

        //等待所有子线程准备完成
        System.out.println("主线程等待所有子线程准备完成...");
        readySignal.await();

        System.out.println("主线程发出开始信号...");
        // 所有子线程开始调用接口
        startSignal.countDown();

        // 等待所有子线程完成任务
        doneSignal.await();
        System.out.println("所有子线程已完成接口调用...");
    }
}

运行结果:

主线程等待所有子线程准备完成...
Thread-0, 已准备就绪,等待开始信号...
Thread-1, 已准备就绪,等待开始信号...
Thread-2, 已准备就绪,等待开始信号...
Thread-3, 已准备就绪,等待开始信号...
Thread-4, 已准备就绪,等待开始信号...
主线程发出开始信号...
时间戳1746787080788,Thread-0 开始调用接口...
时间戳1746787080789,Thread-4 开始调用接口...
时间戳1746787080788,Thread-1 开始调用接口...
时间戳1746787080788,Thread-2 开始调用接口...
时间戳1746787080788,Thread-3 开始调用接口...
所有子线程已完成接口调用...
注意事项

CountDownLatch的await()方法和Object中的wait()方法极为相似,一不小心很容易调用到wait()方法。两个方法都是可以让线程进入等待状态,但是两者的底层实现线程等待的方式还是有区别的,根据已掌握的知识,我们总结下两者的部分区别,后面深入学习后再进一步总结。

(1)两者都可以迫使当前线程进入等待状态,两者都可以通过重载的方法指定等待时间。

(2)两者都可以抛出中断异常。

(3)wait()是java.lang.Object类的方法,java中所有对象都继承该方法;await()是java.util.concurrent.CountDownLatch类中的方法。

(4)调用wait()方法前,线程必须先通过synchronized获得锁;而await()方法不用。

(5)调用wait()方法的线程需要其他线程调用notify()/notifyAll()唤醒,而调用await()方法的线程在计数器减到0后自动唤醒。

总结

本文分析了CountDownLatch的的主要方法和使用场景,其主要应用场景是主线程等待各个子线程执行完成后继续执行、多个线程需要等待统一的指令后同时执行。最后总结了CountDownLatch的await()方法和Object中的wait()方法的一些区别。

如果文章对您有帮助,不妨“帧栈”一下,关注“帧栈”公众号,第一时间获取推送文章,您的肯定将是我写作的动力!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

帧栈

您的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值