Java 多线程并发控制工具 CountDownLatch,实现原理及案例
闭锁(CountDownLatch)是 Java 多线程并发中的一种同步器,它是 JDK 内置的同步器。通过它可以定义一个倒计数器,当倒计数器的值大于 0 时,所有调用 await 方法的线程都会等待。而调用 countDown 方法则可以让倒计数器的值减一,当倒计数器值为 0 时所有等待的线程都将继续往下执行。
闭锁的主要应用场景是让某个或某些线程在某个运行节点上等待 N 个条件都满足后才让所有线程继续往下执行,其中倒计数器的值为 N,每满足一个条件倒计数器就减一。比如下图中,倒计数器初始值为 3,然后三个线程调用 await 方法后都在等待。随后倒计数器减一为 2,再减一为 1,最后减一为 0,所有等待的线程都往下继续执行。
三要素
闭锁的三要素为:倒计数器的初始值、await 方法以及 countdown 方法。倒计数器的初始值在构建 CountDownLatch 对象时指定,它表示我们需要等待的条件个数。await 方法能让线程进入等待状态,等待的条件是倒计数器的值大于 0。countdown 方法用于将倒计数器的值减一。
实现原理
先前介绍过如何基于 AQS 同步器实现一个自定义同步器,实际上 CountdownLatch 也是基于 AQS 来实现的,只要使用 AQS 的共享模式即可以轻松实现闭锁。
下面我们看详细的实现代码,CountdownLatch 类的构造函数需要传入一个整型参数,表示倒计数器的初始值,对应着 AQS 的 state 状态变量。按照官方推荐的自定义同步器的做法,将继承了 AQS 类的子类 Sync 作为 CountdownLatch 类的内部类,而 CountdownLatch 同步器中相关的操作只需代理成子类中对应的方法即可。比如 await 方法和 countDown 方法分别调用 Sync 子类的 acquireSharedInterruptibly 方法和 releaseShared 方法。
Sync 子类中需要我们实现的两个方法是 tryAcquireShared 和 tryReleaseShared,分别用于获取共享锁和释放共享锁。先看获取共享锁的逻辑,如果状态变量等于 0 则返回 1,当倒计数器的值减少到 0 的时候全部线程都可以直接尝试得到共享锁,而当倒计数器的值为非 0 时使之返回-1 交给 AQS 进行入队管理。然后看释放共享锁的逻辑,主要是通过自旋来进行减一操作,getState 方法获取状态变量,将其值减一后使用 compareAndSetState 方法进行 CAS 修改状态值。
案例 1
第一个例子是创建一个 CountdownLatch 对象作为倒计数器,其值为 2。然后线程一调用 await 方法进行等待,线程二调用 countDown 方法将倒计数器的值减一并往下执行。线程三再调用 countDown 方法将倒计数器的值再减一并往下执行,此时倒计数器的值为 0,线程一停止等待并往下执行。
下面的例子输出如下,thread1 调用 await 方法后进入等待状态,thread2 睡眠两秒后调用 countDown 方法并往下执行,thread3 睡眠四秒后调用 countDown 方法并往下执行,最后 thread1 才停止等待继续往下执行。
案例 2
第二个例子是创建一个 CountdownLatch 对象作为倒计数器,其值为 2。然后线程一和线程二都分别调用 await 方法进行等待,线程三则调用两次 countDown 方法将倒计数器的值减二并往下执行,此时倒计数器的值为 0,线程一和线程二停止等待并往下执行。
下面的例子输出如下,thread1 和 thread2 调用 await 方法后进入等待状态,然后主线程调用 countDown 方法后休眠两秒钟。此时的倒计数器值为 1,所以 thread1 和 thread2 继续等待,直到主线程休眠结束后再次调用 countDown 方法后,thread1 和 thread2 才能继续往下执行。
总结
CountDownLatch 中文名为闭锁,是 JDK 内置的一个同步器,它通过倒计数器的值来控制多线程并发。它主要由倒计数器的初始值、await 操作和 countdown 操作。内部的实现是基于 AQS 同步器,并且我们提供了两个例子让大家了解闭锁的使用场景和使用方法。
版权声明: 本文为 InfoQ 作者【码农架构】的原创文章。
原文链接:【http://xie.infoq.cn/article/af89076b2bdce72b323dfbe18】。文章转载请联系作者。
评论