CountDownLatch
CountDownLatch可以实现一个线程等待其他线程执行完毕之后再执行;它内部维护着一个未完成操作的计数器,默认值为构造的参数,当执行CountDownLatch.countDown()方法的时候,计数器就会减1;CountDownLatch.await()方法相当于一个受保护的方法,当计数器减到为0时,就进行notify,告知之前执行过await的线程可以继续执行了;这样看来,它的执行原理和wait、notify是一样的,就是在其基础上增加了一个计数器的概念。
使用的demo如下,模拟王者荣耀进入界面:
@Slf4j
public class CountdownLatchTest {
public static void main(String[] args) throws InterruptedException {
//模拟王者荣耀的例子
//设置一个数组
String[] all = new String[10];
//设置一个随机数
Random random = new Random();
//创建线程池
ExecutorService pool = Executors.newFixedThreadPool(10);
//设置倒计时锁
CountDownLatch countDownLatch = new CountDownLatch(10);
//十个线程开始执行
for (int i = 0; i < 10; i++) {
//lambda表达式里面只能引用局部常量
int i1 = i;
pool.submit(() -> {
for (int j = 0; j <= 100; j++) {
all[i1] = j + "%";
try {
//随机生成0到100的任意整数
Thread.sleep(random.nextInt(100));
} catch (InterruptedException e) {
e.printStackTrace();
}
//“\r”是让后面的打印直接回退到刚开始的一行
System.out.print("\r" + Arrays.toString(all));
}
//任务执行完毕之后-1
countDownLatch.countDown();
});
}
//countDown减到0时,该线程才能继续执行。
countDownLatch.await();
System.out.println("\n游戏开始");
//关闭线程池
pool.shutdown();
}
需要注意的几点:
1、为了避免程序出现错误,无限制的等待下去,可以使用 countDownLatch.await(long,TimeUnit)方法设置一个超时时间;
2、使用的时候尽量避免执行次数小于计数器个数的情况,如果不设置过期时间,则等待线程将会永远等待下去(相反,计数器也不会出现负数的情况);如下错误的情况:
ExecutorService pool = Executors.newFixedThreadPool(4);
CountDownLatch latch = new CountDownLatch(5);
//只会执行4次,但计数器为5个,所以不会减少的0;等待线程将会永远等待。
for (int i = 0; i < 4; i++) {
pool.submit(()->{
System.out.println("!!!!");
});
latch.countDown();
}
latch.await();
3、同一个线程也是可以多次执行countDown的,多线程情况下注意不要重复的执行。
CountDownLatch latch = new CountDownLatch(5);
//同一个线程多次执行countDown方法,也是可以的。
new Thread(()->{
for (int i = 0; i < 4; i++) {
System.out.println("!!!!");
latch.countDown();
}
}).start();
latch.await();
CyclicBarrier
CyclicBarrier使用的场景是一组线程必须都达到了某种状态,才能同时执行。
就像大家约好了吃饭,必须等到人都齐了才能吃。使用CyclicBarrier实现等待的线程被称作参与方,参与方只要执行CyclicBarrier.await()就可以实现等待。但其更希望使用的场景是一组操作,但是可以执行多次。
入门小demo,等人齐了再吃饭:
public class CyclicBarrierDemo {
private static CyclicBarrier cyclicBarrier;
public static void main(String[] args) {
doAct();
}
public static void doAct() {
cyclicBarrier = new CyclicBarrier(5, new Thread(() -> {
System.out.println("所有人都齐了,可以吃饭了。");
}));
Random random = new Random();
for (int i = 0; i < 5; i++) {
int finalI = i;
new Thread(() -> {
try {
//每个人到目的地花费的时间不同
Thread.sleep(random.nextInt(10));
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(finalI + "到了");
try {
cyclicBarrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}).start();
}
}
}
打印结果:
稍微复杂一些的demo:模拟士兵打靶,每一排士兵同时开始打靶,并都结束后再换下一排士兵,依次重复执行。
@Slf4j
public class ShootPractise {
//参与打靶训练的全部士兵
final Soldier[][] rank;
//靶子的个数,即每排士兵的个数
final int N;
//打靶的持续时间
final int lasting;
//标识是否继续打靶
volatile boolean done = false;
//用来指示进行下一轮打靶的是哪一排的士兵
volatile int nextLine = 0;
//这一排士兵同时开始射击
final CyclicBarrier shiftBarrier;
//等待该排所有士兵射击完成后,同时撤离打靶位置
final CyclicBarrier startBarrier;
public ShootPractise(int n, final int lineCount, int lasting) {
this.N = n;
this.lasting = lasting;
//用二维数组表示所有士兵
this.rank = new Soldier[lineCount][N];
for (int i = 0; i < lineCount; i++) {
for (int j = 0; j < N; j++) {
//赋值
rank[i][j] = new Soldier(i * N + j);
}
}
//N表示有多少个线程需要同时开始,这里是每一排士兵的个数。
shiftBarrier = new CyclicBarrier(N, new Runnable() {
//只有最后一个线程才会执行该方法
@Override
public void run() {
//更新下一轮打靶的排
nextLine = (nextLine + 1) % lineCount;
log.info("Next line is :{}", nextLine);
}
});
startBarrier = new CyclicBarrier(N);
}
public static void main(String[] args) throws InterruptedException {
//一共5排,每一排有4个人
ShootPractise shootPractise = new ShootPractise(4, 5, 24);
shootPractise.start();
}
public void start() throws InterruptedException {
//创建并启动工作者线程
Thread[] threads = new Thread[N];
for (int i = 0; i < N; i++) {
threads[i] = new Shooting(i);
threads[i].start();
}
//指定时间停止后打靶
Thread.sleep(lasting * 5);
stop();
//让当前排打完靶子
for (Thread thread : threads) {
thread.join();
}
System.out.print("Practise finished");
}
public void stop() {
done = true;
}
class Shooting extends Thread {
final int index;
public Shooting(int index) {
this.index = index;
}
@SneakyThrows
@Override
public void run() {
Soldier soldier;
while (!done) {
soldier = rank[nextLine][index];
//一排中的士兵必须同时开始射击
startBarrier.await();
soldier.fire();
//一排中的士兵必须等待该排中的所有士兵射击完成后才能离开射击点
shiftBarrier.await();
}
}
}
static class Soldier {
private final int seqNo;
private void fire() {
System.out.println("soldier"+seqNo + "fire!");
}
public Soldier(int seqNo) {
this.seqNo = seqNo;
}
}
}
二者的区别
1、从实现上来说,countDownLatch是无法被重复使用的,当计数器减少到0之后,就不会发生变化了,而CyclicBarrier是可以重复使用的;这也是和CountDownLatch的主要区别,其内部使用了分代的概念,当前分代的初始状态parties为参与方的总数,CyclicBarrier.await()每被执行一次,parities就会减1,最后一个线程相当于通知线程,它执行CyclicBarrier.await()之后,会使parties变成0,此时该线程再执行barrierAction.run()—构造的第二个参数,默认为null,然后再执行signalAll()来唤醒所有等待线程;之后开始下一个分代,parties又恢复了初始值。
2、从业务上来说,countDownLatch类似于wait/nofity、条件变量,只是希望一组线程执行完毕之后在执行某件事情;而CyclicBarrier更多是希望一种迭代使用,一类反复的操作,就比如上述的士兵打靶。
感谢观看,如有问题一起探讨。