大家好,我是程序员牛肉。
JUC作为Java面试的必考板块,其重要性不言而喻。学习JUC包下的常用类不仅仅是在学习这些类怎么使用,更是在学习这些类中所蕴藏的设计思维。
而今天我们要介绍的两个类分别是“CountDownLatch”和“CyclicBairrier”。
我们先来介绍CountDownLatch。我们设想这样一个业务场景:我们的代码中需要执行三个任务A,B,C。
在这其中,任务B的执行需要A,C任务执行得到的结果。那么最简单的执行逻辑就应该是这样:
可是这样串行执行也太low了。身为一名合格的程序员,我必须使用多线程了:我们把任务A和任务C调成为子线程异步执行。
于是我们的执行逻辑变成了这样:
可这样也不对啊,还记得我们之前说过任务B的执行需要依赖任务A和任务C的执行结果吗?
主线程是没办法直接执行任务B的。也就是说在我们异步处理执行任务A和任务C的同时,还要设计代码逻辑使得主进程等待任务A和任务C的执行完毕。
在主线程内使用join方法吗?这也太low了。而且这段代码会频繁的创建两个线程用来异步执行任务A和C。
[在 Java 中,join 方法是 Thread 类的一个实例方法,它的作用是让当前线程等待调用 join 方法的线程终止。换句话说,如果一个线程 A 调用了另一个线程 B 的 join 方法,那么线程 A 会一直等待,直到线程 B 执行完毕。]
public class TaskExecution {
public static void main(String[] args) {
// 创建线程执行任务 A
Thread threadA = new Thread(() -> {
执行任务A
});
// 创建线程执行任务 C
Thread threadC = new Thread(() -> {
执行任务C
});
// 启动任务 A 和 C 的线程
threadA.start();
threadC.start();
threadA.join();
threadC.join();
// 任务 A 和 C 完成后执行任务 B
执行任务B
System.out.println("任务 B 开始执行");
}
}
为了避免频繁创建和销毁线程所带来的性能消耗,我们想到了线程池。可是如果使用线程池就又会出现一个问题:
我们本来设计使用join来等待任务A和任务C的结束,但是对线程池的线程使用Join会存在很多的隐患。
比如由于线程池中的线程一直处于复用状态,可能不会真正的退出。那么我们的Join就没有办法准确的检测到任务A和任务C的执行完成。
我们得重新设计一种方案了。其实很容易就能想到计数器:
我们可以搞一个计数器,计数器的初始值设置为2。无论是任务A还是任务C执行完毕,都要在方法内部对这个计数器减一。
而我们的主线程会一直自旋等待这个计数器。只有当计数器的值为0的时候,主线程才会执行方法B。
仔细一想,这个方案其实还挺优秀的。他在一定程度上实现了主线程需要等待任务A和任务C执行完毕的代码逻辑。
如果你能够想到这里,恭喜你设计出了“CountDownLatch”。
[CountDownLatch 是 Java 并发包 java.util.concurrent 中的一个同步辅助类,它主要用于控制一个或多个线程等待其他线程完成某些操作。CountDownLatch 通过一个计数器来实现线程之间的协调,计数器的初始值等于需要等待的事件的数量。]
基于CountDownLatch,我们可以爆改我们的那段代码:
public class TaskExecution {
public static void main(String[] args) throws InterruptedException {
// 创建 CountDownLatch,初始计数为 2
CountDownLatch latch = new CountDownLatch(2);
// 创建线程执行任务 A
Thread threadA = new Thread(() -> {
执行任务A
// A 完成后调用 countDown
latch.countDown();
});
// 创建线程执行任务 C
Thread threadC = new Thread(() -> {
执行任务C
// C 完成后调用 countDown
latch.countDown();
});
// 启动任务 A 和 C 的线程
threadA.start();
threadC.start();
// 主线程等待 A 和 C 完成
latch.await();
// 任务 A 和 C 完成后执行任务 B
System.out.println("任务 B 开始执行");
执行任务B
}
}
让我们最后总结一下CountDownLatch有哪些特点:
需要注意的是,CountDownLatch是一次性的。当一个CountDown被使用完毕之后,我们是没有办法通过重新赋值来重新使用它的。只能重新new一个CountDownLatch。
CountDown就介绍到这里了,下面让我们介绍一下CyclicBairrier。
如果说CountDownLatch强调的是让单个或多个线程等待一组任务的完成,那么CyclicBarrier强调的就是让一组线程互相等待,直到所有线程都到达某个点。
他听起来和CountDownLatch的作用差不多。二者的主要区别也在于CyclicBarrier是可以复用的。
围绕在CyclicBarrier这个类上的主要有两个点:
我们可以简单的写一个代码案例,创建三个线程来相互等待。
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
public class CyclicBarrierExample {
public static void main(String[] args) {
int parties = 3;
CyclicBarrier barrier = new CyclicBarrier(parties, new Runnable() {
@Override
public void run() {
System.out.println("所有线程都到达屏障点,继续执行。");
}
});
for (int i = 0; i < parties; i++) {
new Thread(new Task(barrier)).start();
}
}
}
class Task implements Runnable {
private CyclicBarrier barrier;
public Task(CyclicBarrier barrier) {
this.barrier = barrier;
}
@Override
public void run() {
try {
System.out.println(Thread.currentThread().getName() + " 正在等待屏障。");
barrier.await();
System.out.println(Thread.currentThread().getName() + " 已经跨过屏障。");
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
}
}
相信通过我的介绍,你已经大致了什么是“CountDownLatch”和“CyclicBarrier”。希望我的文章可以帮到你。
关于这两个JUC下的常用类,你有什么想说的嘛?欢迎在评论区留言。
关注我,带你了解更多计算机干货。