46、锁存器和栅栏
在平时开发中,有时候我们需要自己编写代码,测试某个接口在不同并发下的性能
比如测试在 N 个(N 可以是 100、200 等任意数)接口请求并发执行时接口的响应时间、QPS 等
为了模拟接口的并发请求,我们需要用到多线程,除此之外,线程之间的协调执行,还需要用到本节要讲的 CountDownLatch(锁存器)和 CyclicBarrier(栅栏)
接下来我们就结合这个需求的开发,详细讲解一下 CountDownLatch 和 CyclicBarrier 的用法以及实现原理
- CountDownLatch 内部维护了一个计数器,通过调用 countDown() 方法来减少计数器的值,当计数器的值变为 0 时,所有调用 await() 的线程都会被唤醒
- CyclicBarrier 内部维护了一个计数器,通过调用 await() 方法来来减少计数器的值并阻塞当前线程,当计数器的值变为 0 时,所有调用 await() 的线程都会被唤醒
1、CountDownLatch 的用法
CountDownLatch 内部维护了一个计数器,通过调用 countDown() 方法来减少计数器的值,当计数器的值变为 0 时,所有调用 await() 的线程都会被唤醒
1.1、介绍
CountDownLatch 中文名称叫作锁存器,其提供的常用方法有以下几个
// 构造函数, 传入 count 值
public CountDownLatch(int count);
// 阻塞等待 count 值变为 0
public void await() throws InterruptedException;
public boolean await(long timeout, TimeUnit unit) throws InterruptedException;
// 将 count 值减一
public void countDown();
CountDownLatch 的作用有点类似 Thread 类中的 join() 函数,用于一个线程等待其他多个线程的事件发生
对于 join() 函数来说,这里的事件指的是线程结束,对于 CountDownLatch 来说,这里的事件可以根据业务逻辑来定义
除此之外,使用 join() 需要知道被等待的线程是谁,而使用 CountDownLatch 则不需要,因此 CountDownLatch 相对于 join() 函数来说更加通用
1.2、示例
我们举个例子解释一下,代码如下所示
- 在 DemoJoin 中,主线程(执行 main() 函数的线程)调用 join() 函数阻塞等待线程 t1 和 t2 的结束
- 在 DemoLatch 中,CountDownLatch 中的 count 值初始化为 2,主线程调用 await() 函数阻塞等待 count 值变为 0
 另外两个线程在执行完部分逻辑之后,调用 countDown() 函数将 count 值减一
 当两个线程均执行 countDown() 函数之后,count 值变为 0,阻塞在 await() 函数上的主线程被唤醒,继续执行后续逻辑
public class DemoJoin {
    public static class RunnableForJoin implements Runnable {
        @Override
        public void run() {
            // ... do something ...
        }
    }
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(new RunnableForJoin());
        Thread t2 = new Thread(new RunnableForJoin());
        t1.start();
        t2.start();
        t1.join(); // join() 只用来等待线程执行结束, 并且必须知道被等待线程是谁
        t2.join();
    }
}
public class DemoLatch {
    private static final CountDownLatch latch = new CountDownLatch(2);
    public static class RunnableForLatch implements Runnable {
        @Override
        public void run() {
            // ... do something ...
            latch.countDown();
            // ... do other thing ...
        }
    }
    public static void main(String[] args) throws InterruptedException {
        new Thread(new RunnableForLatch()).start();
        new Thread(new RunnableForLatch()).start();
        latch.await(); // 等待 something 执行完成而非等待线程结束, 并且不需要知道在等谁
        // ... 执行后续逻辑 ...
    }
}
2、CountDownLatch 的实现原理
2.1、源码
CountDownLatch 的用法非常简单,其实现原理也不难,底层依赖 AQS 来实现,CountDownLatch 的部分源码如下所示
AQS 模板方法的使用方法是比较固定的,因此 CountDownLatch 的代码结构跟之前讲过的 ReentrantLock、ReentrantReadWriteLock、Semaphore 的代码结构是类似的
代码结构大致为:具体化抽象模板方法类 AQS,在具体类 Sync 中实现 AQS 中的抽象方法,使用具体类 Sync 中的模板方法来编程
public class CountDownLatch {
    // 具体化抽象模板方法类 AQS
    private static final class Sync extends AbstractQueuedSynchronizer {
        Sync(int count) {
            setState(count);// 将 count 值存储在 AQS 的 state 中
        }
        // 实现 AQS 的抽象方法
        protected int tryAcquireShared(int acquires) {
            return (getState() == 0) ? 1 : -1; // 检查 count 是不是为 0 了
        }
        // 实现 AQS 的抽象方法
        protected boolean tryReleaseShared(int releases) {
            for (; ; ) { // 执行 count--
                int c = getState();
                if (c == 0) return false;
                int nextc = c - 1;
                if (compareAndSetState(c, nextc)) return nextc == 0;
            }
        }
    }
    private final Sync sync;
    public CountDownLatch(int count) {
        if (count < 0) throw new IllegalArgumentException("count < 0");
        this.sync = new Sync(count);
    }
    public void await() throws InterruptedException {
        sync.acquireSharedInterruptibly(1); // 使用 Sync 的模板方法编程
    }
    public boolean await(long timeout, TimeUnit unit) throws InterruptedException {
        // 使用 Sync 的模板方法编程
        return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
    }
    public void countDown() {
        sync.releaseShared(1); // 使用 Sync 的模板方法编程
    }
}
我们重点看下 await() 和 countDown() 这两个函数的实现原理
2.2、await()
await() -> acquireSharedInterruptibly() -> tryAcquireShared() -> doAcquireSharedInterruptibly()
从上述代码,我们可以发现,await() 函数直接调用 AQS 的 acquireSharedInterruptibly() 函数,acquireSharedInterruptibly() 函数的源码如下所示
CountDownLatch 的 count 值存储在 AQS 的 state 中,acquireSharedInterruptibly() 函数调用 tryAcquireShared() 函数查看 state 是否为 0
如果 state 为 0,则直接返回,如果 state 不为 0,则调用 doAcquireSharedInterruptibly() 函数阻塞等待 state 变为 0
tryAcquireShared() 函数在上述 CountDownLatch 的 Sync 内部类中已经给出
对于 doAcquireSharedInterruptibly() 函数,其跟我们之前在讲解 ReentrantReadWriteLock 实现原理时讲到的 doAcquireShared() 函数类似,读者可以自行查阅源码了解
public final void acquireSharedInterruptibly(int arg) throws InterruptedException {
    if (Thread.interrupted()) throw new InterruptedException();
    if (tryAcquireShared(arg) < 0) // 查看 state 是否为 0, 否则就放入等待队列等待 state 为 0
        doAcquireSharedInterruptibly(arg); // 阻塞等待 state 为 0
}
2.3、countDown()
countDown() -> releaseShared() -> tryReleaseShared() -> doReleaseShared()
我们再来看下 CountDownLatch 中的 countDown() 函数,countDown() 函数直接调用 AQS 的 releaseShared() 函数来实现,releaseShared() 函数的源码如下所示
releaseShared() 函数调用 tryReleaseShared() 函数将 state 减一
如果此时 state 变为 0,则执行 doReleaseShared() 函数唤醒等待队列中的线程,也就是唤醒调用了 await() 函数的线程
tryReleaseShared() 函数在上述 CountDownLatch 的 Sync 内部类中已经给出
对于 doReleaseShared() 函数,其在我们讲解 ReentrantReadWriteLock 实现原理时已经详细讲解,这里就不再赘述了
public final boolean releaseShared(int arg) {
    // state--, 如果 state 变为 0, 则执行 doReleaseShared()
    if (tryReleaseShared(arg)) {
        doReleaseShared(); // 唤醒等待队列中的线程
        return true;
    }
    return false;
}
3、CyclicBarrier 的用法
CyclicBarrier 内部维护了一个计数器,通过调用 await() 方法来来减少计数器的值并阻塞当前线程,当计数器的值变为 0 时,所有调用 await() 的线程都会被唤醒
CyclicBarrier 的字面意思是可循环使用(Cyclic)的屏障(Barrier)
它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程于会继续运行
3.1、介绍
接下来我们再来看下 CyclicBarrier,CyclicBarrier 包含的常用方法有如下所示
// 构造函数, 传入 parties
public CyclicBarrier(int parties);
// 构造函数, 用于在线程到达屏障时, 优先执行 barrierAction, 方便处理更复杂的业务场景
public CyclicBarrier(int parties, Runnable barrierAction);
// 调用 await() 函数的线程会将 parties 减一, 如果不为 0, 则阻塞, 直到为 0 为止
public int await() throws InterruptedException, BrokenBarrierException;
public int await(long timeout, TimeUnit unit) throws InterruptedException, BrokenBarrierException, TimeoutException;
CyclicBarrier 的中文名为栅栏,非常形象地解释了 CyclicBarrier 的作用,用于多个线程互相等待,互相等待的线程都就位之后,再同时开始执行
3.2、示例 1
我们举个例子解释一下,如下代码所示
我们创建了一个 parties 为 10 的 CyclicBarrier 对象,用于 10 个线程之间互相等待
尽管这 10 个线程启动(执行 start() 函数)的时间不同,但每个线程启动之后,都会调用 await() 函数,将 parties 减一,然后检查 parties 是否为 0
如果 parties 不为 0,则当前线程阻塞等待,如果 parties 为 0,则当前线程唤醒所有调用了 await() 函数的线程
public class Demo {
    private static final CyclicBarrier barrier = new CyclicBarrier(10);
    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        barrier.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace(); // 当前线程被中断
                    } catch (BrokenBarrierException e) {
                        e.printStackTrace(); // 其他线程调用 await() 期间被中断
                    }
                    // 执行业务逻辑
                }
            }).start();
        }
        // 主线程需要等待以上 10 个线程执行结束, 方法有以下 3 种
        // 1、Sleep()
        // 2、join()
        // 3、CountDownLatch
    }
}
3.3、示例 2
对于 CountDownLatch 和 CyclicBarrier,前者是用于一个线程阻塞等待其他线程,后者是用于多个线程互相等待
使用 CountDownLatch,也可以实现 CyclicBarrier 所能实现的功能,如下代码所示
我们创建一个 count 值为 1 的 CountDownLatch 对象,10 个线程均调用 await() 函数阻塞等待 count 为 0
主线程调用 countDown() 函数将 count 值减一,变为 0,然后唤醒调用了 await() 函数的这 10 个线程,以此达到让这 10 个线程同步执行的目的
public class Demo {
    private static final CountDownLatch latch = new CountDownLatch(1);
    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        latch.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    // 执行业务逻辑
                }
            }).start();
        }
        latch.countDown();
        // 主线程需要等待以上 10 个线程执行结束, 方法有以下 3 种
        // 1、Sleep()
        // 2、join()
        // 3、CountDownLatch
    }
}
4、CyclicBarrier 的实现原理
CyclicBarrier 的使用方法比较简单,接下来我们来看下它的实现原理
跟 CountDownLatch 不同,CyclicBarrier 并未使用 AQS 来实现,而是使用之前讲到条件变量来实现的
CyclicBarrier 类的源码如下所示,为了更清晰地展示其核心实现原理,我对 CyclicBarrier 中的源码做了简化
调用 await() 函数先将 parties 减一,然后检查 parties 是否为 0
- 如果不为 0,则调用 Condition 上的 await() 函数让当前线程阻塞等待
- 如果为 0,则调用 Condition 上的 signalAll() 函数唤醒所有调用了 await() 函数的线程
public class CyclicBarrier {
    private final ReentrantLock lock = new ReentrantLock();
    private final Condition trip = lock.newCondition();
    private int parties;
    public CyclicBarrier(int parties) {
        this.parties = parties;
    }
    // 函数返回值: 线程调用 await() 函数之后的 parties 剩余值
    // 注意: 以下代码逻辑省略掉了对 InterruptedException 和 BrokenBarrierException 的处理
    public int await() throws InterruptedException, BrokenBarrierException {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            int index = --parties;
            if (index == 0) {  // 最后一个调用 await() 的线程, 唤醒其他线程
                trip.signalAll();
                return 0;
            }
            trip.await();
            return index;
        } finally {
            lock.unlock();
        }
    }
}
5、在接口性能测试中的应用
对 CountDownLatch 和 CyclicBarrier 的用法和实现原理有了了解之后,接下来我们使用 CountDownLatch 和 CyclicBarrier 来编写开头的接口并发性能测试代码
如果要测试 N 个接口请求并发执行时接口的性能,我们需要创建 N 个测试线程,让每个测试线程循环执行接口请求,并且记录每个接口请求的响应时间
主线程通过 CountDownLatch 来等待其他测试线程执行完成,然后再通过记录的运行数据,统计接口的性能,比如:平均响应时间、QPS 等
除此之外,在以下代码实现中,我们还使用了 CyclicBarrier,让各测试线程更加精确地同时开始执行,以便更加准确地测试指定并发下的接口性能
public class ApiBenchmark {
    private static int numThread = 20;         // 并发度为 20
    private static int numReqPerThread = 1000; // 每个线程请求 1000 次接口
    private static CountDownLatch latch = new CountDownLatch(numThread); // 锁存器: 等待各测试线程完成后,唤醒主线程
    private static CyclicBarrier barrier = new CyclicBarrier(numThread); // 栅栏: 让各测试线程更加精确地同时开始执行
    public static class TestRunnable implements Runnable {
        public List<Long> respTimes = new ArrayList<>();
        @Override
        public void run() {
            try {
                barrier.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (BrokenBarrierException e) {
                e.printStackTrace();
            }
            for (int i = 0; i < numReqPerThread; i++) {
                long reqStartTime = System.nanoTime();
                // ... 调用接口
                long reqEndTime = System.nanoTime();
                respTimes.add(reqEndTime - reqStartTime);
            }
            latch.countDown();
        }
    }
    public static void main(String[] args) throws InterruptedException {
        // 创建线程
        Thread[] threads = new Thread[numThread];
        TestRunnable[] runnables = new TestRunnable[numThread];
        for (int i = 0; i < numThread; i++) {
            runnables[i] = new TestRunnable();
            threads[i] = new Thread(runnables[i]);
        }
        // 启动线程
        long startTime = System.nanoTime();
        for (int i = 0; i < numThread; i++) {
            threads[i].start();
        }
        // 等待测试线程结束
        latch.await();
        long endTime = System.nanoTime();
        // 统计接口性能
        long qps = (numThread * numReqPerThread * 1000) / ((endTime - startTime) / 1000000);
        float avgRespTime = 0.0f;
        for (int i = 0; i < numThread; i++) {
            for (Long respTime : runnables[i].respTimes) {
                avgRespTime += respTime;
            }
        }
        avgRespTime /= (numThread * numReqPerThread);
    }
}
6、课后思考题
在本节中我们讲到,CountDownLatch 底层使用 AQS 来实现,CyclicBarrier 底层使用条件变量来实现,那么本节中提到的 join() 函数是怎么实现的呢
public final void join() throws InterruptedException {
    join(0);
}
public final synchronized void join(final long millis) throws InterruptedException {
    if (millis > 0) {
        if (isAlive()) {
            final long startTime = System.nanoTime();
            long delay = millis;
            do {
                wait(delay);
            } while (isAlive() && (delay = millis -
                    TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime)) > 0);
        }
    } else if (millis == 0) {
        while (isAlive()) {
            wait(0);
        }
    } else {
        throw new IllegalArgumentException("timeout value is negative");
    }
}
join() 采用 Object 上的 wait() 函数实现,它是基于 Java 内置条件变量实现的,当线程终止时,会调用线程自身的 notifyAll() 方法

public synchronized Object get(long millis) throws InterruptedException {
    // 1、加锁
    long future = System.currentTimeMillis() + millis;
    long remaining = millis;
    // 2、检查状态变量是否满足条件, while 循环
    while ((result == null) && remaining > 0) {
        wait(remaining); // 3、等待并释放锁 4、被唤醒之后重新竞争获取锁
        remaining = future - System.currentTimeMillis();
    }
    // 以下为业务逻辑
    return result;
    // 5、解锁
}
本文来自博客园,作者:lidongdongdong~,转载请注明原文链接:https://www.cnblogs.com/lidong422339/p/17490838.html

 
                
            
         浙公网安备 33010602011771号
浙公网安备 33010602011771号