CountDownLatch和CyclicBarrier 傻傻的分不清?超长精美图文又来了
你有一个思想,我有一个思想,我们交换后,一个人就有两个思想
If you can NOT explain it simply, you do NOT understand it well enough
现陆续将Demo代码和技术文章整理在一起 Github实践精选 ,方便大家阅读查看,本文同样收录在此,觉得不错,还请Star
前言
并发编程的三大核心是分工
,同步
和互斥
。在日常开发中,经常会碰到需要在主线程中开启多个子线程去并行的执行任务,并且主线程需要等待所有子线程执行完毕再进行汇总的场景,这就涉及到分工与同步的内容了
在讲 有序性可见性,Happens-before来搞定 时,提到过 join() 规则,使用 join() 就可以简单的实现上述场景:
@Slf4j
public class JoinExample {
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
log.info("Thread-1 执行完毕");
}
}, "Thread-1");
Thread thread2 = new Thread(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
log.info("Thread-2 执行完毕");
}
}, "Thread-2");
thread1.start();
thread2.start();
thread1.join();
thread2.join();
log.info("主线程执行完毕");
}
}
运行结果:
整个过程可以这么理解
我们来查看 join() 的实现源码:
其实现原理是不停的检查 join 线程是否存活,如果 join 线程存活,则 wait(0) 永远的等下去,直至 join 线程终止后,线程的 this.notifyAll() 方法会被调用(该方法是在 JVM 中实现的,JDK 中并不会看到源码),退出循环恢复主线程执行。很显然这种循环检查的方式比较低效
除此之外,使用 join() 缺少很多灵活性,比如实际项目中很少让自己单独创建线程(原因在 我会手动创建线程,为什么要使用线程池? 中说过)而是使用 Executor, 这进一步减少了 join() 的使用场景,所以 join() 的使用在多数是停留在 demo 演示上
那如何实现文中开头提到的场景呢?
CountDownLatch
CountDownLatch, 直译过来【数量向下门闩】,那肯定里面有计数器的存在了。我们将上述程序用 CountDownLatch 实现一下,先让大家有个直观印象
@Slf4j
public class CountDownLatchExample {
private static CountDownLatch countDownLatch = new CountDownLatch(2);
public static void main(String[] args) throws InterruptedException {
// 这里不推荐这样创建线程池,最好通过 ThreadPoolExecutor 手动创建线程池
ExecutorService executorService = Executors.newFixedThreadPool(2);
executorService.submit(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
log.info("Thread-1 执行完毕");
//计数器减1
countDownLatch.countDown();
}
});
executorService.submit(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
log.info("Thread-2 执行完毕");
//计数器减1
countDownLatch.countDown();
}
});
log.info("主线程等待子线程执行完毕");
log.info("计数器值为:" + countDownLatch.getCount());
countDownLatch.await();
log.info("计数器值为:" + countDownLatch.getCount());
log.info("主线程执行完毕");
executorService.shutdown();
}
}
运行结果如下:
结合上述示例的运行结果,相信你也能猜出 CountDownLatch 的实现原理了:
- 初始化计数器数值,比如为2
- 子线程执行完则调用
countDownLatch.countDown()
方法将计数器数值减1 - 主线程调用 await() 方法阻塞自己,直至计数器数值为0(即子线程全部执行结束)
不知道你是否注意,
countDownLatch.countDown();
这行代码可以写在子线程执行的任意位置,不像 join() 要完全等待子线程执行完,这也是 CountDownLatch 灵活性的一种体现
上述的例子还是过于简单,Oracle 官网 CountDownLatch 说明 有两个非常经典的使用场景,示例很简单,强烈建议查看相关示例代码,打开使用思路。我将两个示例代码以图片的形式展示在此处:
官网示例1
- 第一个是开始信号
startSignal
,阻止任何工人Worker
继续工作,直到司机Driver
准备好让他们继续工作 - 第二个是完成信号
doneSignal
,允许司机Driver
等待,直到所有的工人Worker
完成。
官网示例2
另一种典型的用法是将一个问题分成 N 个部分 (比如将一个大的 list 拆分成多分,每个 Worker 干一部分),Worker 执行完自己所处理的部分后,计数器减1,当所有子部分完成后,Driver 才继续向下执行
结合官网示例,相信你已经可以结合你自己的业务场景解,通过 CountDownLatch 解决一些串行瓶颈来提高运行效率了,会用还远远不够,咱得知道 CountDownLatch 的实现原理
源码分析
CountDownLatch 是 AQS 实现中的最后一个内容,有了前序文章的知识铺垫:
当你看到 CountDownLatch 的源码内容,你会高兴的笑起来,内容真是太少了
展开类结构全部内容就这点东西
既然 CountDownLatch 是基于 AQS 实现的,那肯定也离不开对同步状态变量 state 的操作,我们在初始化的时候就将计数器的值赋值给了state
另外,它可以多个线程同时获取,那一定是基于共享式获取同步变量的用法了,所以它需要通过重写下面两个方法控制同步状态变量 state :
- tryAcquireShared()
- tryReleaseShared()
CountDownLatch 暴露给使用者的只有 await()
和 countDown()
两个方法,前者是阻塞自己,因为只有获取同步状态才会才会出现阻塞的情况,那自然是在 await()
的方法内部会用到 tryAcquireShared()
;有获取就要有释放,那后者 countDown()
方法内部自然是要用到 tryReleaseShared()
方法了
PS:如果你对上面这个很自然的推断理解有困难,强烈建议你看一下前序文章的铺垫,以防止知识断层带来的困扰
await()
先来看 await() 方法, 从方法签名上看,该方法会抛出 InterruptedException, 所以它是可以响应中断的,这个我们在 Java多线程中断机制 中明确说明过
public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
其内部调用了同步器提供的模版方法 acquireSharedInterruptibly
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
// 如果监测到中断标识为true,会重置标识,然后抛出 InterruptedException
if (Thread.interrupted())
throw new InterruptedException();
// 调用重写的 tryAcquireShared 方法,该方法结果如果大于零则直接返回,程序继续向下执行,如果小于零,则会阻塞自己
if (tryAcquireShared(arg) < 0)
// state不等于0,则尝试阻塞自己
doAcquireSharedInterruptibly(arg);
}
重写的 tryAcquireShared
方法非常简单, 就是判断同步状态变量 state 的值是否为 0, 如果为零 (子线程已经全部执行完毕)则返回1, 否则返回 -1
protected int tryAcquireShared(int acquires) {
return (getState() == 0) ? 1 : -1;
}
如果子线程没有全部执行完毕,则会通过 doAcquireSharedInterruptibly
方法阻塞自己,这个方法在 Java AQS共享式获取同步状态及Semaphore的应用分析 中已经仔细分析过了,这里就不再赘述了
private void doAcquireSharedInterruptibly(int arg)
throws InterruptedException {
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head) {
// 再次尝试获取同步装阿嚏,如果大于0,说明子线程全部执行完毕,直接返回
int r = tryAcquireShared(arg);
if (r >= 0) {
setHeadAndPropagate(node, r);
p.next = null; // help GC
failed = false;
return;
}
}
// 阻塞自己
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
await()
方法的实现就是这么简单,接下来看看 countDown()
的实现原理
countDown()
public void countDown() {
sync.releaseShared(1);
}
同样是调用同步器提供的模版方法 releaseShared
public final boolean releaseShared(int arg) {
// 调用自己重写的同步器方法
if (tryReleaseShared(arg)) {
// 唤醒调用 await() 被阻塞的线程
doReleaseShared();
return true;
}
return false;
}
重写的 tryReleaseShared
同样很简单
protected boolean tryReleaseShared(int releases) {
// Decrement count; signal when transition to zero
for (;;) {
int c = getState();
// 如果当前状态值为0,则直接返回 (1)
if (c == 0)
return false;
// 使用 CAS 让计数器的值减1 (2)
int nextc = c-1;
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}
代码 (1) 判断当前同步状态值,如果为0 则直接返回 false;否则执行代码 (2),使用 CAS 将计数器减1,如果 CAS 失败,则循环重试,最终返回 nextc == 0
的结果值,如果该值返回 true,说明最后一个线程已调用 countDown() 方法,然后就要唤醒调用 await() 方法被阻塞的线程,同样由于分析过 AQS 的模版方法 doReleaseShared 整个释放同步状态以及唤醒的过程,所以这里同样不再赘述了
仔细看CountDownLatch重写的 tryReleaseShared
方法,有一点需要和大家说明:
代码 (1)
if (c == 0)
看似没什么用处,其实用处大大滴,如果没有这个判断,当计数器值已经为零了,其他线程再调用 countDown 方法会将计数器值变为负值
现在就差 await(long timeout, TimeUnit unit)
方法没介绍了
await(long timeout, TimeUnit unit)
public boolean await(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
}
该方法签名同样抛出 InterruptedException,意思可响应中断。它其实就是 await() 更完善的一个版本,简单来说就是
主线程设定等待超时时间,如果该时间内子线程没有执行完毕,主线程也会直接返回
我们将上面的例子稍稍修改一下你就会明白(主线程超时时间设置为 2 秒,而子线程要 sleep 5 秒)
@Slf4j
public class CountDownLatchTimeoutExample {
private static CountDownLatch countDownLatch = new CountDownLatch(2);
public static void main(String[] args) throws InterruptedException {
// 这里不推荐这样创建线程池,最好通过 ThreadPoolExecutor 手动创建线程池
ExecutorService executorService = Executors.newFixedThreadPool(2);
executorService.submit(() -> {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
log.info("Thread-1 执行完毕");
//计数器减1
countDownLatch.countDown();
}
});
executorService.submit(() -> {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
log.info("Thread-2 执行完毕");
//计数器减1
countDownLatch.countDown();
}
});
log.info("主线程等待子线程执行完毕");
log.info("计数器值为:" + countDownLatch.getCount());
countDownLatch.await(2, TimeUnit.SECONDS);
log.info("计数器值为:" + countDownLatch.getCount());
log.info("主线程执行完毕");
executorService.shutdown();
}
}
运行结果如下:
形象化的展示上述示例的运行过程
小结
CountDownLatch 的实现原理就是这么简单,了解了整个实现过程后,你也许发现了使用 CountDownLatch 的一个问题:
计数器减 1 操作是一次性的,也就是说当计数器减到 0, 再有线程调用 await() 方法,该线程会直接通过,不会再起到等待其他线程执行结果起到同步的作用了
为了解决这个问题,贴心的 Doug Lea 大师早已给我们准备好相应策略 CyclicBarrier
本来想将
CyclicBarrier
的内容放到下一个章节,但是 CountDownLatch 的内容着实有些少,不够解渴,另外有对比才有伤害,所以内容没结束,咱得继续看CyclicBarrier
)
CyclicBarrier
上面简单说了一下 CyclicBarrier 被创造出来的理由,这里先看一下它的字面解释:
概念总是有些抽象,我们将上面的例子用 CyclicBarrier 再做个改动,先让大家有个直观的使用概念
@Slf4j
public class CyclicBarrierExample {
// 创建 CyclicBarrier 实例,计数器的值设置为2
private static CyclicBarrier cyclicBarrier = new CyclicBarrier(2);
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(2);
int breakCount = 0;
// 将线程提交到线程池
executorService.submit(() -> {
try {
log.info(Thread.currentThread() + "第一回合");
Thread.sleep(1000);
cyclicBarrier.await();
log.info(Thread.currentThread() + "第二回合");
Thread.sleep(2000);
cyclicBarrier.await();
log.info(Thread.currentThread() + "第三回合");
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
});
executorService.submit(() -> {
try {
log.info(Thread.currentThread() + "第一回合");
Thread.sleep(2000);
cyclicBarrier.await();
log.info(Thread.currentThread() + "第二回合");
Thread.sleep(1000);
cyclicBarrier.await();
log.info(Thread.currentThread() + "第三回合");
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
});
executorService.shutdown();
}
}
运行结果:
结合程序代码与运行结果,我们可以看出,子线程执行完第一回合后(执行回合所需时间不同),都会调用 await() 方法,等所有线程都到达屏障点后,会突破屏障继而执行第二回合,同样的道理最终到达第三回合
形象化的展示上述示例的运行过程
看到这里,你应该明白 CyclicBarrier 的基本用法,但随之你内心也应该有了一些疑问:
- 怎么判断所有线程都到达屏障点的?
- 突破某一屏障后,又是怎么重置 CyclicBarrier 计数器,等待线程再一次突破屏障呢?
带着这些问题我们来看一看源码
源码分析
同样先打开 CyclicBarrier 的类结构,展开类全部内容,其实也没多少内容
从类结构中看到有:
- await() 方法,猜测应该和 CountDownLatch 是类似的,都是获取同步状态,阻塞自己
- ReentrantLock,CyclicBarrier 内部竟然也用到了我们之前讲过的 ReentrantLock,猜测这个锁一定保护 CyclicBarrier 的某个变量,那肯定也是基于 AQS 相关知识了
- Condition,存在条件,猜测会有等待/通知机制的运用
我们继续带着这些猜测,结合上面的实例代码一点点来验证
// 创建 CyclicBarrier 实例,计数器的值设置为2
private static CyclicBarrier cyclicBarrier = new CyclicBarrier(2);
查看构造函数 (这里的英文注释舍不得删掉,因为说的太清楚了,我来结合注释来说明一下):
private final int parties;
private int count;
public CyclicBarrier(int parties) {
this(parties, null);
}
/**
* Creates a new {@code CyclicBarrier} that will trip when the
* given number of parties (threads) are waiting upon it, and which
* will execute the given barrier action when the barrier is tripped,
* performed by the last thread entering the barrier.
*
* @param parties the number of threads that must invoke {@link #await}
* before the barrier is tripped
* @param barrierAction the command to execute when the barrier is
* tripped, or {@code null} if there is no action
* @throws IllegalArgumentException if {@code parties} is less than 1
*/
public CyclicBarrier(int parties, Runnable barrierAction) {
if (parties <= 0) throw new IllegalArgumentException();
this.parties = parties;
this.count = parties;
this.barrierCommand = barrierAction;
}
根据注释说明,parties 代表冲破屏障之前要触发的线程总数,count 本身又是计数器,那问题来了
直接就用 count 不就可以了嘛?为啥同样用于初始化计数器,要维护两个变量呢?
从 parties 和 count 的变量声明中,你也能看出一些门道,前者有 final 修饰,初始化后就不可以改变了,因为 CyclicBarrier 的设计目的是可以循环利用的,所以始终用 parties 来记录线程总数,当 count 计数器变为 0 后,如果没有 parties 的值赋给它,怎么进行重新复用再次计数呢,所以这里维护两个变量很有必要
接下来就看看 await() 到底是怎么实现的
// 从方法签名上可以看出,该方法同样可以被中断,另外还有一个 BrokenBarrierException 异常,我们一会看
public int await() throws InterruptedException, BrokenBarrierException {
try {
// 调用内部 dowait 方法, 第一个参数为 false,表示不设置超时时间,第二个参数也就没了意义
return dowait(false, 0L);
} catch (TimeoutException toe) {
throw new Error(toe); // cannot happen
}
}
接下来看看 dowait(false, 0L)
做了哪些事情 (这个方法内容有点多,别担心,逻辑并不复杂,请看关键代码注释)
private int dowait(boolean timed, long nanos)
throws InterruptedException, BrokenBarrierException,
TimeoutException {
final ReentrantLock lock = this.lock;
// 还记得之前说过的 Lock 标准范式吗? JDK 内部都是这么使用的,你一定也要遵循范式
lock.lock();
try {
final Generation g = generation;
// broken 是静态内部类 Generation唯一的一个成员变量,用于记录当前屏障是否被打破,如果打破,则抛出 BrokenBarrierException 异常
// 这里感觉挺困惑的,我们要【冲破】屏障,这里【打破】屏障却抛出异常,注意我这里的用词
if (g.broken)
throw new BrokenBarrierException();
// 如果线程被中断,则会通过 breakBarrier 方法将 broken 设置为true,也就是说,如果有线程收到中断通知,直接就打破屏障,停止 CyclicBarrier, 并唤醒所有线程
if (Thread.interrupted()) {
breakBarrier();
throw new InterruptedException();
}
// ************************************
// 因为 breakBarrier 方法在这里会被调用多次,为了便于大家理解,我直接将 breakBarrier 代码插入到这里
private void breakBarrier() {
// 将打破屏障标识 设置为 true
generation.broken = true;
// 重置计数器
count = parties;
// 唤醒所有等待的线程
trip.signalAll();
}
// ************************************
// 每当一个线程调用 await 方法,计数器 count 就会减1
int index = --count;
// 当 count 值减到 0 时,说明这是最后一个调用 await() 的子线程,则会突破屏障
if (index == 0) { // tripped
boolean ranAction = false;
try {
// 获取构造函数中的 barrierCommand,如果有值,则运行该方法
final Runnable command = barrierCommand;
if (command != null)
command.run();
ranAction = true;
// 激活其他因调用 await 方法而被阻塞的线程,并重置 CyclicBarrier
nextGeneration();
// ************************************
// 为了便于大家理解,我直接将 nextGeneration 实现插入到这里
private void nextGeneration() {
// signal completion of last generation
trip.signalAll();
// set up next generation
count = parties;
generation = new Generation();
}
// ************************************
return 0;
} finally {
if (!ranAction)
breakBarrier();
}
}
// index 不等于0, 说明当前不是最后一个线程调用 await 方法
// loop until tripped, broken, interrupted, or timed out
for (;;) {
try {
// 没有设置超时时间
if (!timed)
// 进入条件等待
trip.await();
else if (nanos > 0L)
// 否则,判断超时时间,这个我们在 AQS 中有说明过,包括为什么最后超时阈值 spinForTimeoutThreshold 不再比较的原因,大家会看就好
nanos = trip.awaitNanos(nanos);
} catch (InterruptedException ie) {
// 条件等待被中断,则判断是否有其他线程已经使屏障破坏。若没有则进行屏障破坏处理,并抛出异常;否则再次中断当前线程
if (g == generation && ! g.broken) {
breakBarrier();
throw ie;
} else {
Thread.currentThread().interrupt();
}
}
if (g.broken)
throw new BrokenBarrierException();
// 如果新一轮回环结束,会通过 nextGeneration 方法新建 generation 对象
if (g != generation)
return index;
if (timed && nanos <= 0L) {
breakBarrier();
throw new TimeoutException();
}
}
} finally {
lock.unlock();
}
}
doWait 就是 CyclicBarrier 的核心逻辑, 可以看出,该方法入口使用了 ReentrantLock,这也就是为什么 Generation broken 变量没有被声明为 volatile 类型保持可见性,因为对其的更改都是在锁的内部,同样在锁的内部对计数器 count 做更新,也保证了原子性
doWait 方法中,是通过 nextGeneration 方法来重新初始化/重置 CyclicBarrier 状态的,该类中还有一个 reset() 方法,也是重置 CyclicBarrier 状态的
public void reset() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
breakBarrier(); // break the current generation
nextGeneration(); // start a new generation
} finally {
lock.unlock();
}
}
但 reset() 方法并没有在 CyclicBarrier 内部被调用,显然是给 CyclicBarrier 使用者来调用的,那问题来了
什么时候调用 reset() 方法呢
正常情况下,CyclicBarrier 是会被自动重置状态的,从 reset 的方法实现中可以看出调用了 breakBarrier
方法,也就是说,调用 reset 会使当前处在等待中的线程最终抛出 BrokenBarrierException 并立即被唤醒,所以说 reset() 只会在你想打破屏障时才会使用
上述示例,我们构建 CyclicBarrier 对象时,并没有传递 barrierCommand 对象, 我们修改示例传入一个 barrierCommand 对象,看看会有什么结果:
// 创建 CyclicBarrier 实例,计数器的值设置为2
private static CyclicBarrier cyclicBarrier = new CyclicBarrier(2, () -> {
log.info("全部运行结束");
});
运行结果:
从运行结果中来看,每次冲破屏障后都会执行 CyclicBarrier 初始化 barrierCommand 的方法, 这与我们对 doWait() 方法的分析完全吻合,从上面的运行结果中可以看出,最后一个线程是运行 barrierCommand run() 方法的线程,我们再来形象化的展示一下整个过程
从上图可以看出,barrierAction 与每次突破屏障是串行化的执行过程,假如 barrierAction 是很耗时的汇总操作,那这就是可以优化的点了,我们继续修改代码
// 创建单线程线程池
private static Executor executor = Executors.newSingleThreadExecutor();
// 创建 CyclicBarrier 实例,计数器的值设置为2
private static CyclicBarrier cyclicBarrier = new CyclicBarrier(2, () -> {
executor.execute(() -> gather());
});
private static void gather() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("全部运行结束");
}
我们这里将 CyclicBarrier 的回调函数 barrierAction使用单线程的线程池,这样最后一个冲破屏障的线程就不用等待 barrierAction 的执行,直接分配个线程池里的线程异步执行,进一步提升效率
运行结果如下:
我们再形象化的看一下整个过程:
这里使用了单一线程池,增加了并行操作,提高了程序运行效率,那问题来了:
如果 barrierAction 非常非常耗时,冲破屏障的任务就可能堆积在单一线程池的等待队列中,就存在 OOM 的风险,那怎么办呢?
这是就要需要一定的限流策略或者使用线程池的拒绝的略等
那把单一线程池换成非单一的固定线程池不就可以了嘛?比如 fixed(5)
乍一看确实能缓解单线程池可能引起的任务堆积问题,上面代码我们看到的 gather() 方法,假如该方法内部没有使用锁或者说存在竟态条件,那 CyclicBarrier 的回调函数 barrierAction 使用多线程必定引起结果的不准确
所以在实际使用中还要结合具体的业务场景不断优化代码,使之更加健壮
总结
本文讲解了 CountDownLatch 和 CyclicBarrier 的经典使用场景以及实现原理,以及在使用过程中可能会遇到的问题,比如将大的 list 拆分作业就可以用到前者,读取多个 Excel 的sheet 页,最后进行结果汇总就可以用到后者 (文中完整示例代码已上传)
最后,再形象化的比喻一下
- CountDownLatch 主要用来解决一个线程等待多个线程的场景,可以类比旅游团团长要等待所有游客到齐才能去下一个景点
- 而 CyclicBarrier 是一组线程之间的相互等待,可以类比几个驴友之间的不离不弃,共同到达某个地方,再继续出发,这样反复
灵魂追问
- 怎样拿到 CyclicBarrier 的汇总结果呢?
- 线程池中的 Future 特性你有使用过吗?
接下来,咱们就聊聊那些可以使用的 Future 特性
参考
- Java 并发编程实战
- Java 并发编程的艺术
- Java 并发编程之美
- When to reset CyclicBarrier in java multithreading
个人博客:https://dayarch.top
加我微信好友, 进群娱乐学习交流,备注「进群」
欢迎持续关注公众号:「日拱一兵」
- 前沿 Java 技术干货分享
- 高效工具汇总 | 回复「工具」
- 面试问题分析与解答
- 技术资料领取 | 回复「资料」
以读侦探小说思维轻松趣味学习 Java 技术栈相关知识,本着将复杂问题简单化,抽象问题具体化和图形化原则逐步分解技术问题,技术持续更新,请持续关注......
CountDownLatch和CyclicBarrier 傻傻的分不清?超长精美图文又来了的更多相关文章
- Qemu,KVM,Virsh傻傻的分不清
当你安装了一台Linux,想启动一个KVM虚拟机的时候,你会发现需要安装不同的软件,启动虚拟机的时候,有多种方法: virsh start kvm命令 qemu命令 qemu-kvm命令 qemu-s ...
- 并发工具类:CountDownLatch、CyclicBarrier、Semaphore
在多线程的场景下,有些并发流程需要人为来控制,在JDK的并发包里提供了几个并发工具类:CountDownLatch.CyclicBarrier.Semaphore. 一.CountDownLatch ...
- 【Java多线程】JUC包下的工具类CountDownLatch、CyclicBarrier和Semaphore
前言 JUC中为了满足在并发编程中不同的需求,提供了几个工具类供我们使用,分别是CountDownLatch.CyclicBarrier和Semaphore,其原理都是使用了AQS来实现,下面分别进行 ...
- Java并发编程:Semaphore、CountDownLatch、CyclicBarrier
首先我们来实现一个功能:当我们启动一个系统的时候需要初始化许多数据,这时候我们可能需要启动很多线程来进行数据的初始化,只有这些系统初始化结束之后才能够启动系统.其实在Java的类库中已经提供了Sema ...
- CountDownLatch与CyclicBarrier
对于AbstractQueuedSynchronizer衍生出来的并发工具类,这一篇再介绍俩. 场景1:有4个大文件的数据需要统计,最终将所有的统计结果进行加工,得到最后的分析数据.为了加速处理过程, ...
- CountDownLatch、CyclicBarrier、Semaphore共同之处与区别以及各自使用场景
区别 CountDownLatch 使一个线程A或是组线程A等待其它线程执行完毕后,一个线程A或是组线程A才继续执行.CyclicBarrier:一组线程使用await()指定barrier,所有线程 ...
- 并发编程(七)——AbstractQueuedSynchronizer 之 CountDownLatch、CyclicBarrier、Semaphore 源码分析
这篇,我们的关注点是 AQS 最后的部分,共享模式的使用.本文先用 CountDownLatch 将共享模式说清楚,然后顺着把其他 AQS 相关的类 CyclicBarrier.Semaphore 的 ...
- Java多线程:CountDownLatch、CyclicBarrier 和 Semaphore
场景描述: 多线程设计过程中,经常会遇到需要等待其它线程结束以后再做其他事情的情况. 有几种方案: 1.在主线程中设置一自定义全局计数标志,在工作线程完成时,计数减1.主线程侦测该标志是否为0,一 ...
- JUC——线程同步辅助工具类(Semaphore,CountDownLatch,CyclicBarrier)
锁的机制从整体的运行转态来讲核心就是:阻塞,解除阻塞,但是如果仅仅是这点功能,那么JUC并不能称为一个优秀的线程开发框架,然而是因为在juc里面提供了大量方便的同步工具辅助类. Semaphore信号 ...
随机推荐
- 用python实现汉诺塔问题
一.用动画实现汉诺塔问题: import turtle class Stack: def __init__(self): self.items = [] def isEmpty(self): retu ...
- Java实现 LeetCode 793 阶乘函数后K个零 (分析)
793. 阶乘函数后K个零 f(x) 是 x! 末尾是0的数量.(回想一下 x! = 1 * 2 * 3 * - * x,且0! = 1) 例如, f(3) = 0 ,因为3! = 6的末尾没有0:而 ...
- Java实现 第十一届 蓝桥杯 (高职专科组)省内模拟赛
有错误的或者有问题的欢迎评论 十六进制数1949对应的十进制数 19000互质的数的个数 70044与113148的最大公约数 第十层的二叉树 洁净数 递增序列 最大的元素距离 元音字母辅音字母的数量 ...
- Java实现 蓝桥杯VIP 算法训练 打印下述图形
算法训练 4-1打印下述图形 时间限制:1.0s 内存限制:256.0MB 问题描述 使用循环结构打印下述图形,打印行数n由用户输入.打印空格时使用"%s"格式,向printf函数 ...
- Java实现 蓝桥杯VIP 算法训练 新生舞会
问题描述 新生舞会开始了.n名新生每人有三个属性:姓名.学号.性别.其中,姓名用长度不超过20的仅由大小写字母构成的字符串表示,学号用长度不超过10的仅由数字构成的字符串表示,性别用一个大写字符'F' ...
- Java实现 LeetCode 2 两数相加
两数相加 给出两个 非空 的链表用来表示两个非负的整数.其中,它们各自的位数是按照 逆序 的方式存储的,并且它们的每个节点只能存储 一位 数字. 如果,我们将这两个数相加起来,则会返回一个新的链表来表 ...
- 第九届蓝桥杯JavaB组省赛真题
解题代码部分来自网友,如果有不对的地方,欢迎各位大佬评论 题目1.第几天 题目描述 2000年的1月1日,是那一年的第1天. 那么,2000年的5月4日,是那一年的第几天? 注意:需要提交的是一个整数 ...
- Java实现 LeetCode 137 只出现一次的数字
public static int singleNumber(int[] nums) { int res = 0; for(int i=0;i<nums.length;i++) res ^= n ...
- PAT 害死人不偿命的(3n+1)猜想
卡拉兹(Callatz)猜想: 对任何一个正整数 n,如果它是偶数,那么把它砍掉一半:如果它是奇数,那么把 ( 3n+1 )砍掉一半.这样一直反复砍下去,最后一定在某一步得到 n=1.卡拉兹在 195 ...
- MySQL 8.0 yum安装和配置
MySQL 8.0 centos7.5 x86_64 一.yum安装 1.先卸载机器和mysql有关的东西,有的安装了mariab-lib,会对安装有干扰,卸载了它. [root@localhost ...