1 介绍
在上篇文章 逐行源码分析AbstractQueuedSynchronizer(AQS)中Semaphore的源码实现 中,我们分析了高性能源码包java.util.concurrent 中的信号量Semaphore类的使用场景和源码。
这篇文章我们介绍 java.util.concurrent 包中的另一个重要的工具类:CountDownLatch ,这个高性能工具类的使用场景应该是比Semaphore 较于普遍性
2 场景
官方文档中的解释是:
A synchronization aid that allows one or more threads to wait until a set of operations being performed in other threads completes.
即:允许一个或多个线程等待在其他线程中执行的一组操作完成的同步器。
我们试着分析这个例子:
在火箭卫星发射之前,需要针对火箭本身进行各种检测,等各项检测完成之后,才能进行火箭发射。
这个现实中的例子映射到到java线程模型工作中是:
火箭发射我们可以认为是一个线程的执行任务的操作,其需要等待其他一些列线程的任务操作完毕即对火箭本身的各项检测 执行完毕才能进行执行。
这个场景就是使用CountDownLatch的绝佳机会,实际上,无论是业务功能代码还是框架逻辑代码,一个线程需要去等待其他一些列线程操作执行完毕才能执行的场景很是常见。
3 使用
CountDownLatch 内部一共提供了六个公共方法。
即:通过给定的count值实例化一个CountDownLatch对象,线程的方法调用(await) 会一直阻塞直到其CountDownLatch 对象中的count值归零,通过其他线程调用CountDownLatch中的(countDown) 方法。阻塞的唤醒会由最后一个调用(countDown)使其count为0 的线程发起,并唤醒所有阻塞在执行(await)方法的所有线程。
对于类的文字描述可能不及一段代码来的直接
public class CountDownLatchDemo implements Runnable{
static final CountDownLatch end = new CountDownLatch(10);
static final CountDownLatchDemo countDownLatchDemo = new CountDownLatchDemo();
@Override
public void run() {
try{
//模拟检查任务
Thread.sleep(new Random().nextInt(10) * 100);
System.out.println("Current thread check complete");
end.countDown();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws InterruptedException {
ExecutorService executorService = Executors.newFixedThreadPool(10);
for(int i=0;i<10;i++) {
executorService.submit(countDownLatchDemo);
}
end.await();
System.out.println("over");
executorService.shutdown();
//等待检查
}
}
上面代码执行之后会发现:
主线程(main)的 System.out.println(“over”); 的执行是会等到所有线程执行完毕System.out.println(“Current thread check complete”); 之后才会执行
4 共享锁和独占锁
这篇文章不打算一行一行的贴出CountDownLatch的源码介绍,因为CountDownLatch实现的源码实在是过于少。仅仅内部存在一个内部类Sync,实现AQS的基类,并且实现了AQS中的tryReleaseShared 方法。
CountDownLatch的内部逻辑基本上依赖于AQS内部代码逻辑,即:
1,countDown()方法内部通过自旋+cas的方法设置AQS中的state的值(减一)。
2,当自旋 + cas 更新state的值,使其为0时,触发唤醒在阻塞 await() 方法的线程,内部调用doReleaseShared() ,
3,await() 方法 首先是会校验当前CountDownLatch 中的state的值是否为0, 为0(直接执行呀),不为0,通过自旋 + cas 的策略追加到AQS中CLH的等待队列中,并挂起当前线程。
实际上,对于AQS的内部代码逻辑实现,我们还有一个点需要去分析,那就是共享锁和独占锁
对于这个锁类型的区分,AQS内部是通过存放在CLH锁等待队列节点Node的nextWaiter成员变量进行分析,
有意思的是:AQS内部的源码实现,对于这两个锁的释放操作并没有去依赖这个属性进行区分。而是直接通过release(int args) 和 releaseShared(int arg) 硬编码的形式去区分如何释放这两种类型的锁
独占锁模式下的解锁操作:
public final boolean release(int arg) {
// 独占锁模式下的解锁操作
// tryRelease执行,
if (tryRelease(arg)) {
Node h = head;
// 这个写法很牛皮呀
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
上面的这段代码相信已经十分面熟,尤其是我们在分析:逐行源码分析AbstractQueuedSynchronizer(AQS)中ReentrantLock的源码实现 的时候,
因为ReentrantLock就是独占锁模式
对于独占锁模式的释锁过程,
调用独占锁具体实现类实现的tryRelease 方法的时候不会通过自旋 + cas 的操作去 去释放AQS的state值,并且在去唤醒CLH锁等待队列的线程的时候,都是单个唤醒的
共享锁模式下的解锁操作:
public final boolean releaseShared(int arg) {
// 我先尝试一下释放锁, 这个实际上是自旋
if (tryReleaseShared(arg)) {
// 释放锁成功了,唤醒后继节点, 这个操作和ReentrantLock的思路差不多
doReleaseShared();
return true;
}
return false;
}
调用共享锁具体实现类的tryReleaseShared(arg) 方法的时候是通过 自旋+ cas 的操作去释放AQS的state值,并且在去唤醒CLH锁等待队列的线程的时候,都是通过自旋的操作 ,释放所有等待在CLH的线程的
java.util.concurrent常见的共享锁和独占锁的实现:
独占锁:
共享锁: