高性能编程面试点
1、java程序运行原理分析
2、java线程状态
2.1 线程的6种状态
2.2 线程中止/复位-interrupt/使用自定义标识符
告诉线程可以进行中断,是否中断取决于线程本身。保证线程安全性,处于阻塞状态则通过抛异常。。来提示调用者信息,线程会复位,依旧执行继续的逻辑。
使用自定义标识符进行线程中止。
两种复位重置:我有事情没干完呢,不能就这么停了
-
可以通过Therad.interuupted(); 重置
-
InterruptedException来进行重置
2.3 CPU的多级缓存和缓存同步协议
**缓存的同步协议:**volatile的底层实现,通过对缓存标识位的修改实现内存的可见性。
CPU指令重排序:(可以使用volatile、fianllly 禁止指令重排序)
**内存屏障:**java 编译器在生成指令序列时会插入内存屏障来实现禁止指令的重排序
2.4 为什么阻塞方法都会抛出一个异常?
阻塞的方法的结束都需要事件的触发进行结束。阻塞的未能结束正常告知调用者,这个事情怎么做(强制结束、继续运行)提供一个入口。
wait\sleep\join 阻塞方法,正常结束都需要一些条件的执行
notify\时间结束\notify
3、Synchronized关键词
3.1 锁有作用范围那么锁存贮在哪里呢?
锁存贮在java的对象中
3.2 java对象的组成结构
- 锁的状态:区分当前是那种锁
- java 对象头的mark word
- 锁升级:无锁-->{编向锁}--》轻量级锁--》重量级锁
3.3 Synchroinzed锁的优化
-
控制Synchroinzed 的锁的粒度
-
无锁化
-
**偏向锁:**CAS --compare and swaper (value ,except,update);
value --主内存中的值,except --当前内存值 ,value==except ,更新update数据
-
**轻量级锁:**偏向锁升级成轻量级锁时候会清除锁的头信息,使用CAS 进行比较和加锁。cas:自旋。
-
**重量级锁:**自旋会耗费CPU资源,n 多次自旋后还没获得锁资源,锁升级成重量级。锁膨胀----》阻塞线程,挂起。
-
自适应,jdk1.6 以后。。根据上一个自旋锁进行动态判断
-
设置锁的自旋次数
-
监视器对象的实现,monitor
-
3.4 锁升级的过程
工作中可将,编向锁关闭进行优化,自己写的多线程最少用的轻量级锁,a\b 线程交换获取的锁资源。可在jvm 中进行关闭。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SNzeZq3f-1628501370047)(pic.wangt.cc/download/pi…)]
4、wait、notify/notifyall ---线程的通信

线程A :进行wait
线程B :唤醒线程A
4.1 为什么使用wait和notify 需要进行加锁?
线程间需要进行通信和线程间同步,通信是指线程间以何种机制交换信息,同步是指:程序中不同线程间操作顺序发生的相对次序。。。线程间通常用两种进行通信的方法:共享内存和消息传递。
java 中的通信采用共享内存模式,通过写-读公共的状态进行隐式通信,显示同步(程序员必须指明那些代码是在线程间需要互斥进行的)。
消息传递并发模式中:线程间没有中间状态,必须通过发消息进行显示通信。隐式同步
5、JMM内存模型
5.1 可见性的根本原因:高速缓存、重排序
5.2 happens-before原则
- 单线程的线程顺序
- voliate 关键词(内存屏障:强制建数据从分片cpu的高速缓存刷新的主内存中)
- 传递性规则(a before b, b before c ,则a before c)
- 线程start规则(start 前的before start )
- 线程join规则(本质是wait和notfiy)
- synchroized 监视器规则(前一解锁的数据before后面加锁的)
这些工作场景中不需要考虑可见性问题。
5.3 缓存一致性协议
MESI 表示缓存行的四种状态,分别是
- M(Modify) 表示共享数据只缓存在当前 CPU 缓存中,并且是被修改状态,也就是缓存的数据和主内存中的数据不一致
- E(Exclusive) 表示缓存的独占状态,数据只缓存在当前CPU 缓存中,并且没有被修改
- S(Shared) 表示数据可能被多个 CPU 缓存,并且各个缓存中的数据和主内存数据一致
- I(Invalid) 表示缓存已经失效。在 MESI 协议中,每个缓存的缓存控制器不仅知道自己的读写操作,而且也监听(snoop)其它 Cache 的读写操作
对于 MESI 协议,从 CPU 读写角度来说会遵循以下原则: CPU 读请求:缓存处于 M、E、S 状态都可以被读取,I 状态 CPU 只能从主存中读取数据 CPU 写请求:缓存处于 M、E 状态才可以被写。对于 S 状态的写,需要将其他 CPU 中缓存行置为无效才可写 使用总线锁和缓存锁机制之后,CPU 对于内存的操作大概可以抽象成下面这样的结构。从而达到缓存一致性效果

6、Lock
6.1 ReentranLock (重入互斥锁)类图

7、 AQS 同步队列
7.1 同步工具实现的功能
-
独占--》互斥
-
共享--》读写锁
7.2 AQS 的基本实现:一个双向链表
锁的基本要素:
- 一个共享数据记录锁的状态(无锁、有锁)--state (0-无锁状态,>0 有锁状态(可重入行值+1))
- node 表示未获得到锁而被封装成的线程信息,线程获得锁后node被当前链表移除
7.3 ReentrantLock 上锁过程分析
NonfairSync 上锁代码 State 当前资源的锁状态
final void lock() {
if (compareAndSetState(0, 1))//使用乐观锁逻辑进行上锁操作,0 预期值,1 成功时要设置的值。
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
//aqs 中acquire 非公平锁tryAcquire 实际调用方法
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();//当前锁的状态,无锁为0
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {//线程时同一个线程
int nextc = c + acquires;//线程state 值添加
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
//aqs 中对当前没事获得到锁的线程封装成node节点添加到链表中
//封装node
static final Node SHARED = new Node();//共享锁
static final Node EXCLUSIVE = null;//独占锁(aqs上个acquire()使用独占)
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;
if (pred != null) {//链表存在,添加到链表中
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);//自锁
return node;
}
//构建链表
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // 初始化链表
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
// 线程B 添加到链表后,你在试试抢锁,要不就停了吧(阻塞)。
//再试试抢锁的
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();//获得当前节点(封装线程信息)上一个节点信息
if (p == head && tryAcquire(arg)) {//在尝试获取
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;(false、true)for循环的唯一出口
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())//将线程阻塞到这里
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
signal=-1 (表示下一个线程挂起状态),线程B 加入试试失败后将上一个状态改成-1,线程C 加入将B改成-1.
线程A 进行unLock()操作唤醒队列中的线程B(非公平是指其他新添加的线程可能进行插队,而不是队列中的值进行插队)

FairSync 上锁代码
final void lock() {
acquire(1);
}
//公平锁上锁
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
//hasQueuedPredecessors()对列中有线程挂起,不进行插队
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
7.4 ReentrantLock 解锁过程分析
ReentrantLock.unlock 中,调用release()释放锁
public final boolean release(int arg) {
if (tryRelease(arg)) { //释放锁成功
Node h = head; //得到 aqs 中 head 节点
if (h != null && h.waitStatus != 0)//如果 head 节点不为空并且状态不为0
unparkSuccessor(h);//唤醒后续节点
return true;
}
return false;
}
ReentrantLock .tryRelease() 这个方法可以认为是一个设置锁状态的操作,通过将 state 状态减掉传入的参数值(参数是 1),如果结果状态为 0,就将排它锁的 Owner 设置为 null,以使得其它的线程有机会进行执行。 在排它锁中,加锁的时候状态会增加 1(当然可以自己修改这个值),在解锁的时候减掉 1,同一个锁,在可以重入后,可能会被叠加为 2、3、4 这些值,只有 unlock()的次数与 lock()的次数对应才会将 Owner 线程设置为空,而且也只有这种情况下才会返回 true。
protected final boolean tryRelease(int releases)
{
int c = getState() - releases;//可重入性值--
if (Thread.currentThread() !=
getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
unparkSuccessor //唤醒下一个线程
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;//获得 head 节点的状态
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);// 设置 head 节点
状态为 0
Node s = node.next;//得到 head 节点的下一个节点
if (s == null || s.waitStatus > 0) {
//如果下一个节点为 null 或者 status>0 表示 cancelled 状态.
//通过从尾部节点开始扫描,找到距离 head 最近的一个
waitStatus<=0 的节点
s = null;
for (Node t = tail; t != null && t != node; t =
t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null) //next 节点不为空,直接唤醒这个线程即可
LockSupport.unpark(s.thread);
}
7.5 公平锁和非公平的锁的区别:
非公平锁上来就进行比较,插队。公平锁上来先看队列中有没有线程挂起,有挂起直接排到队列后面。
8、Condition ---await/sing/singall
8.1 condition 流程图
await:把当前的线程阻塞挂起
signal:唤醒阻塞的线程

8.2 condition await()阻塞方法源码分析
调用Condition的await()方法(或者以await开头的方法),会使当前线程进入**Condition 的等待队列(单向链表)**并释放锁,同时线程状态变为等待状态。当从 await()方法返回时,当前线程一定获取了Condition 相关联的锁
public final void await() throws InterruptedException {
if (Thread.interrupted()) //表示 await 允许被中断
throw new InterruptedException();
Node node = addConditionWaiter(); //创建一个新的节点,节点状态为 condition,采用的数据结构仍然是链表
int savedState = fullyRelease(node); //释放当前的锁,得到锁的状态,并唤醒 AQS 队列中的一个线程
int interruptMode = 0;
//如果当前节点没有在同步队列上,即还没有被 signal,则将当前线程阻塞
while (!isOnSyncQueue(node)) {//判断这个节点是否在 AQS 队列上,第一次判断的是 false,因为前面已经释放锁了
LockSupport.park(this); //通过 park 挂起当前线程
if ((interruptMode =
checkInterruptWhileWaiting(node)) != 0)
break;
}
// 当这个线程醒来,会尝试拿锁, 当 acquireQueued 返回 false 就是拿到锁了.
// interruptMode != THROW_IE -> 表示这个线程 没有成功将 node 入队,但 signal 执行了 enq 方法让其入队了.
// 将这个变量设置成 REINTERRUPT.
if (acquireQueued(node, savedState) &&
interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
// 如果 node 的下一个等待者不是 null, 则进行清理,清理 Condition 队列上的节点.
// 如果是 null ,就没有什么好清理的了.
if (node.nextWaiter != null) // clean up if
cancelled
unlinkCancelledWaiters();
// 如果线程被中断了,需要抛出异常.或者什么都不做
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
构建Condition 的单向链表---addConditionWaiter
将当前线程封装成node,添加到等待队列中,从而形成一个单向链表
private Node addConditionWaiter() {
Node t = lastWaiter;
// 如 果 lastWaiter 不 等 于 空 并 且waitStatus 不等于 CONDITION 时,把冲好这个节点从链表中移除
if (t != null && t.waitStatus !=
Node.CONDITION) {
unlinkCancelledWaiters();
t = lastWaiter;
}
//构建一个Node,waitStatus=CONDITION。 这里的链表是一个单向的,所以相比 AQS 来说会简单很多
Node node = new
Node(Thread.currentThread(),
Node.CONDITION);
if (t == null)
firstWaiter = node;
else
t.nextWaiter = node;
lastWaiter = node;
return node;
}
释放当前调用await()方法线程的锁--fullyRelease
fullyRelease :彻底释放锁,不管当前线程具有重入了多少次锁,一次性释放掉所有的锁。此时,同步队列会触发锁的释放和重新竞争。ThreadB 获得了锁
final int fullyRelease(Node node) {
boolean failed = true;
try {
int savedState = getState();
//获得重入的次数
if (release(savedState)) {//释放锁并且唤醒下一个同步队列中的线程
failed = false;
return savedState;
} else {
throw new
IllegalMonitorStateException();
}
} finally {
if (failed)
node.waitStatus =
Node.CANCELLED;
}
}
判断当前线程是否在AQS 队列中--isOnSyncQueue
如果不在 AQS 同步队列,说明当前节点没有唤醒去争抢同步锁,所以需要把当前线程阻塞起来,直到其他的线程调用 signal 唤醒如果在 AQS 同步队列,意味着它需要去竞争同步锁去获得执行程序执行权限
-
为什么要做这个判断呢?
原因是在 condition 队列中的节点会重新加入到 AQS 队列去竞争锁。也就是当调用 signal的时候,会把当前节点从 condition 队列转移到 AQS 队列
-
如何去判断ThreadA 这个节点是否存在于 AQS 队列中呢?
- 1、**在Codition 的同步队列中。**如果 ThreadA 的 waitStatus 的状态为 CONDITION,说明它存在于 condition 队列中,不在 AQS 队列。因为AQS 队列的状态一定不可能有 CONDITION
- 2、**当前线程时获得锁的线程。**如果 node.prev 为空,说明也不存在于 AQS 队列,原因是 prev=null 在 AQS 队列中只有一种可能性,就是它是head 节点,head 节点意味着它是获得锁的节点。
- 3、AQS 的特性。如果 node.next 不等于空,说明一定存在于 AQS 队列中,因为只有 AQS 队列才会存在 next 和 prev 的关系
- 4、**遍历AQS 队列查找判断node信息是否相等。**findNodeFromTail,表示从 tail 节点往前扫描 AQS 队列,一旦发现 AQS 队列的节点和当前节点相等,说明节点一定存在于 AQS 队列中
final boolean isOnSyncQueue(Node node) {
if (node.waitStatus ==
Node.CONDITION || node.prev == null)
return false;
if (node.next != null) // If has
successor, it must be on queue
return true;
return findNodeFromTail(node);
}
8.3 Condition.signal 唤醒方法
signal()唤醒等待队列中的节点
public final void signal() {
if (!isHeldExclusively()) //先判断当前线程是否获得了锁,这个判断比较简,直接用获得锁的线程和当前线程相比即可
throw new IllegalMonitorStateException();
Node first = firstWaiter; // 拿到 Condition队列上第一个节点
if (first != null)
doSignal(first);//唤醒队列中的第一节点
}
doSignal() :将node节点从Condition的等待队列中移动AQS 队列中
对 condition 队列中从首部开始的第一个 condition 状态的节点,执行 transferForSignal 操作,将 node 从 condition队列中转换到 AQS 队列中,同时修改 AQS 队列中原先尾节点的状态
private void doSignal(Node first) {
do {
//从 Condition 队列中删除 first 节点
if ( (firstWaiter = first.nextWaiter) == null)
lastWaiter = null; // 将 next 节点设置成 null
first.nextWaiter = null;
} while (!transferForSignal(first) &&
(first = firstWaiter) != null);
}
AQS.transferForSignal():修改node节点状态,添加到AQS队列中。唤醒该节点
final boolean transferForSignal(Node node)
{
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))//更新节点的状态为 0,如果更新失 败,只有一种可能就是节点被 CANCELLED 了
return false;
Node p = enq(node);//调用 enq,把当前节点添加到 AQS 队列。并且返回返回按当前节点的上一个节点,也就是原 tail 节点
int ws = p.waitStatus;// 如果上一个节点的状态被取消了, 或者尝试设置上一 个节点的状态为 SIGNAL 失败了(SIGNAL 表示: 他的 next节点需要停止阻塞),
if (ws > 0|| !compareAndSetWaitStatus(p, ws,Node.SIGNAL))
LockSupport.unpark(node.thread); // 唤醒节点上的线程.
return true; //如果 node 的 prev 节点已经是signal 状态,那么被阻塞的 ThreadA 的唤醒工作由 AQS 队列来完成
}
8.4 被阻塞后队列的唤醒操作
前面在分析 await 方法时,线程会被阻塞。而通过 signal被唤醒之后又继续回到上次执行的逻辑中标注为红色部分的代码。checkInterruptWhileWaiting 这个方法是干嘛呢?其实从名字就可以看出来,就是 ThreadA 在 condition 队列被阻塞的过程中,有没有被其他线程触发过中断请求
public final void await() throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
Node node = addConditionWaiter();
int savedState = fullyRelease(node);
int interruptMode = 0;
while (!isOnSyncQueue(node)) {
LockSupport.park(this);
if ((interruptMode =
checkInterruptWhileWaiting(node)) !
= 0)//阻塞,被唤醒后继续执行代码位置
break;
}
if (acquireQueued(node,
savedState) && interruptMode !=
THROW_IE)
interruptMode = REINTERRUPT;
if (node.nextWaiter != null) //clean up if cancelled
unlinkCancelledWaiters();
if (interruptMode != 0)
reportInterruptAfterWait(interruptM
ode);
}
checkInterruptWhileWaiting
如果当前线程被中断,则调用transferAfterCancelledWait 方法判断后续的处理应该是 抛出 InterruptedException 还是重新中断
private int checkInterruptWhileWaiting(Node node) {
return Thread.interrupted() ?
(transferAfterCancelledWait(node) ?
THROW_IE : REINTERRUPT) :0;
}
final boolean transferAfterCancelledWait(Node node) {
//使用 cas 修改节点状态,如果还能修改成功, 说明线程被中断时, signal 还没有被调 用。
// 这里有一个知识点,就是线程被唤醒,并不一定是在 java 层面执行了 locksupport.unpark,也可能是调用了线程的interrupt()方法,这个方法会更新一个中 断标识,并且会唤醒处于阻塞状态下的线程。
if
(compareAndSetWaitStatus(node,
Node.CONDITION, 0)) {
enq(node); //如果cas成功,则把 node 添加到AQS 队列
return true;
}
//如果 cas 失败,则判断当前 node 是否已经在 AQS 队列上,如果不在,则让给其他线程 执行
//当 node 被触发了 signal 方法时, node 就 会被加到 aqs 队列上
while (!isOnSyncQueue(node))//循 环检测 node 是否已经成功添加到 AQS 队列中。如果没有,则通过 yield,
Thread.yield();
return false;
}
acquireQueued :当前被唤醒的线程去重新抢占锁,并且恢复原本的重入次数状态
reportInterruptAfterWait :根据checkInterruptWhileWaiting 返回的中断标识来判断线程的一个处理逻辑,是重新响应中断还是抛出中断异常。
8.5 整体流程图
阻塞: await()方法中,在线程释放锁资源之后,如果节点不在 AQS 等待队列,则阻塞当前线程,如果在等待队列,则自旋等待尝试获取锁 释放: signal()后,节点会从 condition 队列移动到 AQS等待队列,则进入正常锁的获取流程
9、CountDownLatch ---倒计数器
9.1 CountDownLatch 使用到AQS 的共享锁
倒计数器:允许一个或多个线程一直等待,直到其他的线程操作执行完毕后再执行。

9.2 CountDownLatch 倒计数器--countDown()/await()
countDown() 方法每次调用都会将 state 减 1,直到state 的值为 0;而 await 是一个阻塞方法,当 state 减为 0 的时候, await 方法才会返回。 await 可以被多个线程调用,大家在这个时候脑子里要有个图:所有调用了await 方法的线程阻塞在 AQS 的阻塞队列中,等待条件满足(state == 0),将线程从队列中一个个唤醒过来。
9.3 实现原理分析
1、await 方法
- 使用 Sync 继承AQS 重写共享锁方法
- 判断当前线程是否需要添加到的共享锁队列中
- (构建队列)添加线程节点(共享模式的节点)到队列中
- 自旋-判断当前节点是否获取到锁,未获取锁--阻塞线程
-
2、countDown
- 只有当 state 减为 0 的时候,tryReleaseShared 才返回 true, 否则只是简单的 state = state - 1
- 如果state=0, 则调用doReleaseShared 唤醒处于await状态下的线程
- 将标识为共享状态的锁(PRPAGATE)的节点进行唤醒传播
9.4 共享锁的释放和独占锁释的不同
共享锁的释放和独占锁的释放有一定的差别 前面唤醒锁的逻辑和独占锁是一样,先判断头结点是不是SIGNAL状态,如果是,则修改为0,并且唤醒头结点的下一个节点 PROPAGATE: 标识为PROPAGATE状态的节点,是共享锁模式下的节点状态,处于这个状态下的节点,会对线程的唤醒进行传播
10、Semaphore --限流
**作用:**控制同时访问的线程个数,通过 acquire 获取一个许可,如果没有就等待,通过 release 释放一个许可。有点类似限流的作用。
**实现原理:**从 Semaphore 的功能来看,我们基本能猜测到它的底层实现一定是基于 AQS 的共享所,因为需要实现多个线程共享一个领排池 。创建 Semaphore 实例的时候,需要一个参数 permits,这个基本上可以确定是设置给 AQS 的 state 的,然后每 个线程调用 acquire 的时候,执行 state = state - 1, release 的时候执行 state = state + 1,当然, acquire 的时候,如果 state = 0,说明没有资源了,需要等待其他线程 release。 Semaphore 分公平策略和非公平策略 :区别公平先判断是否有线程正在排队,然后进行CAS 减操作。
11、CyclicBarrier --循环栅栏
CyclicBarrier 的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续工作。 CyclicBarrier 默认的构造方法是 CyclicBarrier(int parties),其参数表示屏障拦截的线程数量,每个线程调用 await 方法告诉 CyclicBarrier 当前线程已经到达了屏障,然后当前线程被阻塞
**使用场景:**当存在需要所有子任务都完成时,才能执行主任务,可多次重入,倒计数器只能使用一次。
使用注意事项:
)对于指定计数值 parties,若由于某种原因,没有足够的线程调用 CyclicBarrier 的 await,则所有调用 await 的线程都会被阻塞; 2)同样的 CyclicBarrier 也可以调用 await(timeout, unit),设置超时时间,在设定时间内,如果没有足够线程到达,则解除阻塞状态,继续工作; 3)通过 reset 重置计数,会使得进入 await 的线程出现BrokenBarrierException; 4 ) 如 果 采 用 是 CyclicBarrier(int parties, RunnablebarrierAction) 构造方法,执行 barrierAction 操作的是最后一个到达的线程
实现原理:
CyclicBarrier 相比 CountDownLatch 来说,要简单很多,源码实现是基于 ReentrantLock 和 Condition 的组合使用。看如下示意图, CyclicBarrier 和 CountDownLatch 是不是很像,只是 CyclicBarrier 可以有不止一个栅栏,因为 它的栅栏(Barrier)可以重复使用(Cyclic)