CountDownLatch闭锁

什么是闭锁

闭锁的作用相当于一扇门:用来同步一个或多个任务,强制他们等待由其他任务执行的一组操作完成在闭锁到达结束状态之前,这扇门一直是关闭的,并且没有任务线程能通过。直到闭锁达到结束状态时,这扇门才会打开,并且允许所有的线程通过。当闭锁到达结束状态后,这扇门将永远保持打开状态,因为闭锁将不会在改变其状态

CountDownLatch是一种灵活的闭锁实现,它的内部提供了一个计数器,该计数器被初始化为一个正数表示需要等待的时间数量。另外它还提供了一个countDown()方法来操作计数器的值,每调用一次countDown方法计数器都会减1表示一个事件已经发生了,而await() 方法等待计数器达到零,这表示所有需要等待的时间都已经发生了。如果计数器的值非零,那么await会一直阻塞直到计数器为零,或者等待中的线程中断,或者等待超时。

这就是CountDownLatch的内部机制,看起来很简单,无非就是阻塞一部分线程让其在达到某个条件之后再执行。但是CountDownLatch的应用场景却比较广泛,只要你脑洞够大利用它就可以玩出各种花样。最常见的一个应用场景是开启多个线程同时执行某个任务,等到所有任务都执行完再统计汇总结果。下图动态演示了闭锁阻塞线程的整个过程。

CountDownLatch的构造方法

上图演示了有5个线程因调用await方法而被阻塞,它们需要等待计数器的值减为0才能继续执行。计数器的初始值在构造闭锁时被指定,后面随着每次countDown方法的调用而减1。下面代码贴出了CountDownLatch的构造方法。

public CountDownLatch(int count) {
        if (count < 0) throw new IllegalArgumentException("count < 0");
        this.sync = new Sync(count);
}

CountDownLatch只有一个带参构造器,必须传入一个大于0的值作为计数器初始值,否则会报错。可以看到在构造方法中只是去new了一个Sync对象并赋值给成员变量sync。和其他同步工具类一样,CountDownLatch的实现依赖于AQS,它是AQS共享模式下的一个应用。CountDownLatch实现了一个内部类Sync并用它去继承AQS,这样就能使用AQS提供的大部分方法了。下面我们就来看一下Sync内部类的代码。

private static final class Sync extends AbstractQueuedSynchronizer {
       //构造函数
        Sync(int count) {
            setState(count);
        }
        //获取当前同步状态
        int getCount() {
            return getState();
        }
        //尝试获取锁
        //返回负值,表示当前线程获取失败
        //返回零值,表示当前线程获取成功,但是后继线程不在获取了
        //返回正直,表示当前线程获取成功,并且后继线程同样可以获取成功
        protected int tryAcquireShared(int acquires) {
            return (getState() == 0) ? 1 : -1;
        }
        //尝试释放锁
        protected boolean tryReleaseShared(int releases) {
             for (;;) {
               //获取同步状态
                int c = getState();
                //如果同步状态为0,则不能再释放了
                if (c == 0)
                    return false;
                //否则同步状态-1
                int nextc = c-1;
                //使用CAS方式更新状态
                if (compareAndSetState(c, nextc))
                    return nextc == 0;
            }
        }
}

可以看到Sync的构造方法会将同步状态的值设置为传入的参数值。之后每次调用countDown方法都会将同步状态的值减1,这也就是计数器的实现原理。

await()方法

在平时使用CountDownLatch工具类时最常用的两个方法就是await方法和countDown方法。调用await方法会阻塞当前线程直到计数器为0,调用countDown方法会将计数器的值减1直到减为0。

//await导致当前线程阻塞,直到计数器减到0,或等待线程被中断或等待超时
public void await() throws InterruptedException {
        //以响应中断中断方式获取
        sync.acquireSharedInterruptibly(1);
}

//以中断模式获取锁(共享模式)
public final void acquireSharedInterruptibly(int arg)
            throws InterruptedException {
        //首先判断线程是否中断,如果是则抛出异常
        if (Thread.interrupted())
            throw new InterruptedException();
        //尝试获取锁
        if (tryAcquireShared(arg) < 0)
            doAcquireSharedInterruptibly(arg);
}

当线程调用await方法时其实是调用到了AQS的acquireSharedInterruptibly方法,该方法是以响应线程中断的方式来获取锁的,上面同样贴出了该方法的代码。我们可以看到在acquireSharedInterruptibly方法首先会去调用tryAcquireShared方法尝试获取锁。

我们看到Sync里面重写的tryAcquireShared方法的逻辑,方法的实现逻辑很简单,就是判断当前同步状态是否为0,如果为0则返回1表明可以获取锁,否则返回-1表示不能获取锁。如果tryAcquireShared方法返回1则线程能够不必等待而继续执行,如果返回-1那么后续就会去调用doAcquireSharedInterruptibly方法让线程进入到同步队列里面等待。这就是调用await方法会阻塞当前线程的原理,下面看看countDown()方法是怎样将阻塞的线程唤醒的。

countDown()方法

//减少计数器
 public void countDown() {
        sync.releaseShared(1);
 }
 
//释放锁操作(共享模式)
public final boolean releaseShared(int arg) {
    //尝试去释放锁    
    if (tryReleaseShared(arg)) {
        //如果释放锁成功,就唤醒其他线程
        doReleaseShared();
        return true;
    }
    return false;
 }

可以看到countDown方法里面调用了releaseShared()方法,该方法同样是AQS里面的方法。releaseShared方法里面首先是调用tryReleaseShared方法尝试释放锁,tryReleaseShared方法如果返回true表示释放成功,返回false表示释放失败,只有当将同步状态减1后该同步状态恰好为0时才会返回true,其他情况都是返回false。那么当tryReleaseShared返回true之后就会马上调用doReleaseShared方法去唤醒同步队列的所有线程。这样就解释了为什么最后一次调用countDown方法将计数器减为0后就会唤醒所有被阻塞的线程。

实际应用

典型用法:将程序分为n个互相独立的可解决任务,每个任务执行完成后,执行countDown()操作,等待问题被解决的任务调用await()方法,等待计数器为0时,开始执行任务。

应用场景:在玩欢乐斗地主时必须等待三个玩家都到齐才可以进行发牌

 

 

 

 

 

 

 

 

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值