Java闭锁之使用CountDownLatch统计任务耗时

1.什么是闭锁

在介绍本篇文章之前,先介绍下什么是闭锁?闭锁是一种同步的工具类,可以延迟线程的进度直到其到达终止状态,可以把闭锁看作是一扇门,在闭锁到达结束状态之前,这扇门一直是关闭的,并且没有任何线程能通过,当到达结束状态时,这扇门会打开允许所有线程通过。需要注意的是,当闭锁到达结束状态后,将不会再改变状态,因此这扇门将会永远保持打开状态。闭锁可以用来确保某些任务直到其他任务都完成才继续执行。

2.闭锁的使用场景

闭锁在开发过程中的用途非常广,总结下来主要有以下几个场景会用到闭锁

2.1.确保某个计算在资源初始后再继续执行

在这种场景下我们可以使用二元闭锁,二元闭锁包括两个状态,可以用来表示“资源已经被初始化”,而所有需要用到资源R的操作都必须在这个闭锁上等待。例如我们需要加载一个配置文件,根据配置文件去做一些操作就可以使用这种闭锁的方式;

2.2.确保某个服务在其依赖的所有其他服务都启动之后才启动。

每个服务都有一个相关的二元闭锁。当启动服务S时,将首先在S依赖的其他服务闭锁上等待,在所有依赖的服务都启动后会释放闭锁S,这样其他依赖S的服务才能继续执行。例如在Android的Aidl服务绑定的时候,客户端绑定服务端获取Binder对象时,大概会有个30ms(不精确)的延迟,如果设计SDK的时候,绑定服务端和启动其他调用服务端的操作又是并行的情况下,就会出现binder为空的情况,这里就可以使用闭锁来实现。具体的实现手法有兴趣的话下次单独出一篇文章介绍。

2.3.等待直到某个操作的所有参与者都就绪再继续执行

这里举一个大家很熟悉的例子,王者荣耀的匹配,每次匹配都需要你自己的队友和对面的对手在内的所有玩家都就绪后才能开始游戏,不然就会出现少打多的滑稽情况了。这种场景下,当所有的玩家都准备就绪时,闭锁将会达到结束状态。

3. CountDownLatch

CountDownLatch 是闭锁的一种实现,可以在我们上面的各种场景中使用,它可以使一个或者多个线程等待一组事件发生,闭锁状态包括一个计数器,该计数器被初始化为一个正数,表示需要等待的事件数量,countDown方法递减计数器,表示有一个事件已经发生了,而await方法会一直阻塞直到计数器达到零。这表示所有等待的事件都已经发生,如果计数器的值非零,那么await会一直阻塞直到计数器为零,或者等待中的线程中断,或等待超时

3.1 使用CountDownLatch统计多个工作线程执行的耗时

大致原理是使用两个闭锁,一个开始门,一个结束门,开始门计数器初始值为1,结束门的初始值为工作线程的数量,所有工作线程首先要做的事情就是在开始门上等待,从而确保所有线程都就绪后才开始执行,每个线程套作的最后一件事情是调用结束门的countDown方法减一。demo代码如下所示:

  public static long timeTasks(int nThreads, final Runnable task) throws InterruptedException {
    
    
        final CountDownLatch startGate = new CountDownLatch(1);
        final CountDownLatch endGate = new CountDownLatch(nThreads);

        for (int i = 0; i < nThreads; i++){
    
    
            Thread t = new Thread(){
    
    
                public void run(){
    
    
                    try {
    
    
                        System.out.println("startGate before========");
                        startGate.await();
                        System.out.println("startGate after========");
                        try{
    
    
                            task.run();
                        }finally {
    
    
                            endGate.countDown();
                            System.out.println("endGate.countDown()========");
                        }
                    } catch (InterruptedException ignored) {
    
    }
                }
            };

            t.start();
        }

        long start = System.nanoTime();
        System.out.println("start========" + start);
        startGate.countDown();
        System.out.println("endGate before========");
        endGate.await();
        long end = System.nanoTime();
        System.out.println("end========" + end);
        return end-start;
    }

注意:上面的代码中,也许读者会发现为啥要在timeTask()中使用闭锁,而不是线程一创建后就立即启动,因为如果在创建线程后就立即启动他们,那么先启动的线程将“领先”后启动的线程,并且活跃的线程数量会随着时间的推移而增加或者减少,竞争程度也在不断发生变化。启动门将使得主线程能够同时释放所有工作线程,而结束门则使主线程能够等待最后一个线程执行完成,而不是顺序地等待每个线程执行完成

猜你喜欢

转载自blog.csdn.net/zxj2589/article/details/130963174