• 你有一个头脑,我有一个头脑,我们交流后,一小我私家就有两个头脑

  • 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 的实现原理了:

  1. 初始化计数器数值,好比为2
  2. 子线程执行完则挪用 countDownLatch.countDown() 方式将计数器数值减1
  3. 主线程挪用 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 实现中的最后一个内容,有了前序文章的知识铺垫:

  • Java AQS行列同步器以及ReentrantLock的应用
  • Java AQS共享式获取同步状态及Semaphore的应用剖析

当你看到 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 的基本用法,但随之你心里也应该有了一些疑问:

  1. 怎么判断所有线程都到达屏障点的?
  2. 突破某一屏障后,又是怎么重置 CyclicBarrier 计数器,守候线程再一次突破屏障呢?

带着这些问题我们来看一看源码

源码剖析

同样先打开 CyclicBarrier 的类结构,睁开类所有内容,实在也没多少内容

从类结构中看到有:

  1. await() 方式,预测应该和 CountDownLatch 是类似的,都是获取同步状态,壅闭自己
  2. ReentrantLock,CyclicBarrier 内部竟然也用到了我们之前讲过的 ReentrantLock,预测这个锁一定珍爱 CyclicBarrier 的某个变量,那一定也是基于 AQS 相关知识了
  3. 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 是一组线程之间的相互守候,可以类比几个驴友之间的不离不弃,配合到达某个地方,再继续出发,这样频频

灵魂追问

  1. 怎样拿到 CyclicBarrier 的汇总效果呢?
  2. 线程池中的 Future 特征你有使用过吗?

接下来,咱们就聊聊那些可以使用的 Future 特征

参考

  1. Java 并发编程实战
  2. Java 并发编程的艺术
  3. Java 并发编程之美
  4. When to reset CyclicBarrier in java multithreading
    小我私家博客:https://dayarch.top
    加我微信密友, 进群娱乐学习交流,备注「进群」

迎接连续关注民众号:「日拱一兵」

  • 前沿 Java 手艺干货分享
  • 高效工具汇总 | 回复「工具」
  • 面试问题剖析与解答
  • 手艺资料领取 | 回复「资料」

以读侦探小说头脑轻松意见意义学习 Java 手艺栈相关知识,本着将庞大问题简朴化,抽象问题详细化和图形化原则逐步剖析手艺问题,手艺连续更新,请连续关注......

,

欧博开户

欢迎进入欧博开户(Allbet Game):www.aLLbetgame.us,欧博官网是欧博集团的官方网站。欧博官网开放Allbet注册、Allbe代理、Allbet电脑客户端、Allbet手机版下载等业务。

Allbet Gaming声明:该文看法仅代表作者自己,与阳光在线无关。转载请注明:allbet电脑版下载:CountDownLatch和CyclicBarrier 傻傻的分不清?超长优美图文又来了
发布评论

分享到:

allbet gmaing开户:【下周公然性别】陈凯琳孕后再升Cup 肚凸踏3吋高踭鞋首度亮相
你是第一个吃螃蟹的人
发表评论

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。