Java5.0在java.util.concurrent 包中提供了多种并发容器类来改进同步容器的性能。本章主要介绍了两种JUC常用类:CountDownLatch和CyclicBarrier。这两种常常作为线程同步类工具去使用。
例如:现在有50个任务,这50个任务在完成之后,才能执行下一个方法,要怎么设计?
答:JDK给我们提供的线程工具类,CountDownLatch和CyclicBarrier,这两个类都可以等到线程完成之后,才去执行某些操作。
既然都可以满足需要,那这两个类有什么区别?
答:主要的区别就是CountDownLatch用完了,就结束了,没法复用。而CyclicBarrier不一样,它可以复用。
接下来我们来详细讲解一下两个类的概念。
CountDownLatch:使一个线程或多个线程等待其他线程各自执行完毕后再执行,通过一个计数器来实现的。
计数器的初始值是线程的数量。每当一个线程执行完毕后,计数器的值就-1,当计数器的值为 0 时,表示所有线程都执行完毕,等待的线程就可以恢复工作。
举个例子:百米赛跑,4名运动员(子线程)听到后同时起跑,,裁判(主线程)要等所有选手到达终点进行汇总排名。
CyclicBarrier:让一组线程到达一个屏障时被阻塞,直到最后一个线程到达屏障时,屏障才会开门
举个例子:张三、李四、王五约好一起吃饭,只有三个人都到了才能开始吃。
从上面的描述可以发现两者的等待主体不同
CountDownLatch调用await()通常是主线程/调用线程(让被调用线程先执行完),所以是主线程/调用线程阻塞
CyclicBarrier调用await()是在任务线程调用的(等待其他任务到达),所以是任务线程阻塞
两者有什么共同点:这两个类都是基于AQS实现的(关于AQS之前的文章有详细介绍到)。
当我们在构建CountDownLatch对象时,传入的值其实就会赋值给AQS的关键变量state
执行countDown方法时,其实就是利用CAS 将state 减一
执行await方法时,其实就是判断state是否为0,不为0则加入到队列中,将该线程阻塞掉(除了头结点)
头节点会一直自旋等待state为0,当state为0时,头节点把剩余的在队列中阻塞的节点也一并唤醒。
CyclicBarrier则利用ReentrantLock和Condition,自身维护了count和parties变量(实现复用)。
每次调用await将count-1,并将线程加入到condition队列上。
等到count为0时,则将condition队列的节点移交至AQS队列,并全部释放。
百米赛跑,4名运动员(子线程)听到后同时起跑,裁判(主线程)要等所有选手到达终点进行汇总排名。
public class CountdownLatchDemo {
public static void main(String[] args) {
ExecutorService service = Executors.newCachedThreadPool();
final CountDownLatch cdAnswer = new CountDownLatch(4);
for (int i = 0; i < 4; i++) {
Runnable runnable = new Runnable() {
@Override
public void run() {
try {
Thread.sleep((long) (Math.random() * 10000));
System.out.println("选手" + Thread.currentThread().getName() + "到达终点");
cdAnswer.countDown();
} catch (Exception e) {
e.printStackTrace();
}
}
};
service.execute(runnable);
}
try {
//调用await()通常是主线程/调用线程(让被调用线程先执行完),所以是主线程阻塞
cdAnswer.await();
System.out.println("所有选手都到达终点,"+"裁判"+Thread.currentThread().getName()+"汇总成绩排名");
} catch (InterruptedException e) {
e.printStackTrace();
}
service.shutdown();
}
}
在构建CountDownLatch对象时,传入的值其实就会赋值给AQS的关键变量state
执行countDown方法时,其实就是利用CAS 将state 减一
5个人约定好一起吃饭,只有5个人全部到达之后才能开始吃饭,先到达的人必须等待。
public class CyclicBarrierDemo {
public static void main(String[] args) {
// 人员数为5,栅栏需要拦截4位提前到达的人员。
int memberCount = 5;
CyclicBarrier cyclicBarrier = new CyclicBarrier(memberCount);
// 开启5个人员线程
for(int i = 1; i <= 5; i++) {
new Thread(() -> {
try {
System.out.println(Thread.currentThread().getName() + "进入饭桌");
//任务线程调用的(等待其他任务到达),任务线程阻塞
cyclicBarrier.await();
// 全部的任务线程(5人)全部到达才能开始
System.out.println(Thread.currentThread().getName() + "开始吃饭");
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}, String.valueOf(i)).start();
}
}
}
默认的构造方法是CyclicBarrier(int parties),其参数表示屏障将同时拦截的线程数量。
总结:每个线程使用await()方法告诉CyclicBarrier我已经到达了屏障,然后开始等待。直到parties个参与线程都调用了await方法,那么栅栏将打开,此时所有被栅栏拦截的线程都将继续往下执行,而栅栏将被重置以便下次使用。
本文讲解了CountDownLatch和CyclicBarrier两个线程同步类工具的含义以及应用场景。JUC常用类还有很多,在以后的章节中会介绍更多的JUC常用类供参考学习。