下面是我在2018年10月11日二面百度的时候的一个问题:

java程序,主进程需要等待多个子进程结束之后再执行后续的代码,有哪些方案可以实现?

这个需求其实我们在工作中经常会用到,比如用户下单一个产品,后台会做一系列的处理,为了提高效率,每个处理都可以用一个线程来执行,所有处理完成了之后才会返回给用户下单成功,欢迎大家批评指正:

1.join方法

使用Thread的join()等待所有的子线程执行完毕,主线程在执行,thread.join()把指定的线程加入到当前线程,可以将两个交替执行的线程合并为顺序执行的线程。比如在线程B中调用了线程A的join()方法,直到线程A执行完毕后,才会继续执行线程B。

示例:

 1 import java.util.Vector;
2
3 public class Test {
4 public static void main(String[] args) throws InterruptedException {
5 Vector<Thread> vector = new Vector<>();
6 for(int i=0;i<5;i++) {
7 Thread childThread= new Thread(new Runnable() {
8
9 @Override
10 public void run() {
11 // TODO Auto-generated method stub
12 try {
13 Thread.sleep(1000);
14 } catch (InterruptedException e) {
15 // TODO Auto-generated catch block
16 e.printStackTrace();
17 }
18 System.out.println("子线程被执行");
19 }
20
21 });
22 vector.add(childThread);
23 childThread.start();
24 }
25 for(Thread thread : vector) {
26 thread.join();
27 }
28 System.out.println("主线程被执行");
29 }

执行结果:

子线程被执行
子线程被执行
子线程被执行
子线程被执行
子线程被执行
主线程被执行

2.等待多线程完成的CountDownLatch

CountDownLatch的概念

CountDownLatch是一个同步工具类,用来协调多个线程之间的同步,或者说起到线程之间的通信(而不是用作互斥的作用)。

CountDownLatch能够使一个线程在等待另外一些线程完成各自工作之后,再继续执行。使用一个计数器进行实现。计数器初始值为线程的数量。当每一个线程完成自己任务后,计数器的值就会减一。当计数器的值为0时,表示所有的线程都已经完成了任务,然后在CountDownLatch上等待的线程就可以恢复执行任务。
CountDownLatch的用法

CountDownLatch典型用法1:某一线程在开始运行前等待n个线程执行完毕。将CountDownLatch的计数器初始化为n new CountDownLatch(n) ,每当一个任务线程执行完毕,就将计数器减1 countdownlatch.countDown(),当计数器的值变为0时,在CountDownLatch上 await() 的线程就会被唤醒。一个典型应用场景就是启动一个服务时,主线程需要等待多个组件加载完毕,之后再继续执行。

CountDownLatch典型用法2:实现多个线程开始执行任务的最大并行性。注意是并行性,不是并发,强调的是多个线程在某一时刻同时开始执行。类似于赛跑,将多个线程放到起点,等待发令枪响,然后同时开跑。做法是初始化一个共享的CountDownLatch(1),将其计数器初始化为1,多个线程在开始执行任务前首先 coundownlatch.await(),当主线程调用 countDown() 时,计数器变为0,多个线程同时被唤醒。
CountDownLatch的不足

CountDownLatch是一次性的,计数器的值只能在构造方法中初始化一次,之后没有任何机制再次对其设置值,当CountDownLatch使用完毕后,它不能再次被使用。

 1 import java.util.Vector;
2 import java.util.concurrent.CountDownLatch;
3
4 public class Test2 {
5 public static void main(String[] args) throws InterruptedException {
6 final CountDownLatch latch = new CountDownLatch(5);
7 for(int i=0;i<5;i++) {
8 Thread childThread= new Thread(new Runnable() {
9
10 @Override
11 public void run() {
12 // TODO Auto-generated method stub
13 try {
14 Thread.sleep(1000);
15 } catch (InterruptedException e) {
16 // TODO Auto-generated catch block
17 e.printStackTrace();
18 }
19 System.out.println("子线程被执行");
20 latch.countDown();
21 }
22
23 });
24
25 childThread.start();
26
27 }
28 latch.await();//阻塞当前线程直到latch中的值
29 System.out.println("主线程被执行");
30 }
31
32 }

执行结果:

子线程被执行
子线程被执行
子线程被执行
子线程被执行
子线程被执行
主线程被执行

3.同步屏障CyclicBarrier

这里必须注意,CylicBarrier是控制一组线程的同步,初始化的参数:5的含义是包括主线程在内有5个线程,所以只能有四个子线程,这与CountDownLatch是不一样的。

countDownLatch和cyclicBarrier有什么区别呢,他们的区别:countDownLatch只能使用一次,而CyclicBarrier方法可以使用reset()方法重置,所以CyclicBarrier方法可以能处理更为复杂的业务场景。

我曾经在网上看到一个关于countDownLatch和cyclicBarrier的形象比喻,就是在百米赛跑的比赛中若使用 countDownLatch的话冲过终点线一个人就给评委发送一个人的成绩,10个人比赛发送10次,如果用CyclicBarrier,则只在最后一个人冲过终点线的时候发送所有人的数据,仅仅发送一次,这就是区别。

 1 package interview;
2
3 import java.util.concurrent.BrokenBarrierException;
4 import java.util.concurrent.CyclicBarrier;
5
6 public class Test3 {
7 public static void main(String[] args) throws InterruptedException, BrokenBarrierException {
8 final CyclicBarrier barrier = new CyclicBarrier(5);
9 for(int i=0;i<4;i++) {
10 Thread childThread= new Thread(new Runnable() {
11
12 @Override
13 public void run() {
14 // TODO Auto-generated method stub
15 try {
16 Thread.sleep(1000);
17 } catch (InterruptedException e) {
18 // TODO Auto-generated catch block
19 e.printStackTrace();
20 }
21 System.out.println("子线程被执行");
22 try {
23 barrier.await();
24 } catch (InterruptedException e) {
25 // TODO Auto-generated catch block
26 e.printStackTrace();
27 } catch (BrokenBarrierException e) {
28 // TODO Auto-generated catch block
29 e.printStackTrace();
30 }
31 }
32
33 });
34
35 childThread.start();
36
37 }
38 barrier.await();//阻塞当前线程直到latch中的值
39 System.out.println("主线程被执行");
40 }
41 }

执行结果:

子线程被执行
子线程被执行
子线程被执行
子线程被执行
主线程被执行

4.使用yield方法(注意此种方法经过亲自试验证明并不可靠!)

 1 public class Test4 {
2 public static void main(String[] args) throws InterruptedException {
3 for(int i=0;i<5;i++) {
4 Thread childThread= new Thread(new Runnable() {
5
6 @Override
7 public void run() {
8 // TODO Auto-generated method stub
9 try {
10 Thread.sleep(1000);
11 } catch (InterruptedException e) {
12 // TODO Auto-generated catch block
13 e.printStackTrace();
14 }
15 System.out.println("子线程被执行");
16
17 }
18
19 });
20
21 childThread.start();
22
23 }
24 while (Thread.activeCount() > 2) { //保证前面的线程都执行完
25 Thread.yield();
26 }
27 System.out.println("主线程被执行");
28 }
29 }

执行结果:

1 子线程被执行
2 子线程被执行
3 子线程被执行
4 子线程被执行
5 主线程被执行
6 子线程被执行

为何yield方法会出现这样的问题?

使当前线程从执行状态(运行状态)变为可执行态(就绪状态)。cpu会从众多的可执行态里选择,也就是说,当前也就是刚刚的那个线程还是有可能会被再次执行到的,并不是说一定会执行其他线程而该线程在下一次中不会执行到了。

Java线程中有一个Thread.yield( )方法,很多人翻译成线程让步。顾名思义,就是说当一个线程使用了这个方法之后,它就会把自己CPU执行的时间让掉,让自己或者其它的线程运行。

打个比方:现在有很多人在排队上厕所,好不容易轮到这个人上厕所了,突然这个人说:“我要和大家来个竞赛,看谁先抢到厕所!”,然后所有的人在同一起跑线冲向厕所,有可能是别人抢到了,也有可能他自己有抢到了。我们还知道线程有个优先级的问题,那么手里有优先权的这些人就一定能抢到厕所的位置吗? 不一定的,他们只是概率上大些,也有可能没特权的抢到了。

yield的本质是把当前线程重新置入抢CPU时间的”队列”(队列只是说所有线程都在一个起跑线上.并非真正意义上的队列)。

5.FutureTast可用于闭锁,类似于CountDownLatch的作用

 1 import java.util.concurrent.Callable;
2 import java.util.concurrent.ExecutionException;
3 import java.util.concurrent.FutureTask;
4
5 public class Test5 {
6 public static void main(String[] args) {
7 MyThread td = new MyThread();
8
9 //1.执行 Callable 方式,需要 FutureTask 实现类的支持,用于接收运算结果。
10 FutureTask<Integer> result1 = new FutureTask<>(td);
11 new Thread(result1).start();
12 FutureTask<Integer> result2 = new FutureTask<>(td);
13 new Thread(result2).start();
14 FutureTask<Integer> result3 = new FutureTask<>(td);
15 new Thread(result3).start();
16
17 Integer sum;
18 try {
19 sum = result1.get();
20 sum = result2.get();
21 sum = result3.get();
22 //这里获取三个sum值只是为了同步,并没有实际意义
23 System.out.println(sum);
24 } catch (InterruptedException e) {
25 // TODO Auto-generated catch block
26 e.printStackTrace();
27 } catch (ExecutionException e) {
28 // TODO Auto-generated catch block
29 e.printStackTrace();
30 } //FutureTask 可用于 闭锁 类似于CountDownLatch的作用,在所有的线程没有执行完成之后这里是不会执行的
31
32 System.out.println("主线程被执行");
33
34 }
35
36 }
37
38 class MyThread implements Callable<Integer> {
39
40 @Override
41 public Integer call() throws Exception {
42 int sum = 0;
43 Thread.sleep(1000);
44 for (int i = 0; i <= 10; i++) {
45 sum += i;
46 }
47 System.out.println("子线程被执行");
48 return sum;
49 }
50 }

6.使用callable+future

Callable+Future最终也是以Callable+FutureTask的形式实现的。

在这种方式中调用了: Future future = executor.submit(task);

 1 import java.util.concurrent.Callable;
2 import java.util.concurrent.ExecutionException;
3 import java.util.concurrent.ExecutorService;
4 import java.util.concurrent.Executors;
5 import java.util.concurrent.Future;
6
7 public class Test6 {
8 public static void main(String[] args) throws InterruptedException, ExecutionException {
9 ExecutorService executor = Executors.newCachedThreadPool();
10 Task task = new Task();
11 Future<Integer> future1 = executor.submit(task);
12 Future<Integer> future2 = executor.submit(task);
13 //获取线程执行结果,用来同步
14 Integer result1 = future1.get();
15 Integer result2 = future2.get();
16
17 System.out.println("主线程执行");
18 executor.shutdown();
19 }
20 }
21 class Task implements Callable<Integer>{
22 @Override public Integer call() throws Exception {
23 int sum = 0;
24 //do something;
25 System.out.println("子线程被执行");
26 return sum;
27 }
28 }

执行结果:

子线程被执行
子线程被执行
主线程执行

补充:

1)CountDownLatch和CyclicBarrier都能够实现线程之间的等待,只不过它们侧重点不同:

CountDownLatch一般用于某个线程A等待若干个其他线程执行完任务之后,它才执行;

而CyclicBarrier一般用于一组线程互相等待至某个状态,然后这一组线程再同时执行;

另外,CountDownLatch是不能够重用的,而CyclicBarrier是可以重用的。

2)Semaphore其实和锁有点类似,它一般用于控制对某组资源的访问权限。

CountDownLatch类实际上是使用计数器的方式去控制的,不难想象当我们初始化CountDownLatch的时候传入了一个int变量这个时候在类的内部初始化一个int的变量,每当我们调用countDownt()方法的时候就使得这个变量的值减1,而对于await()方法则去判断这个int的变量的值是否为0,是则表示所有的操作都已经完成,否则继续等待。
实际上如果了解AQS的话应该很容易想到可以使用AQS的共享式获取同步状态的方式来完成这个功能。而CountDownLatch实际上也就是这么做的。


参考文献:

https://blog.csdn.net/u011277123/article/details/54015755/
https://blog.csdn.net/joenqc/article/details/76794356

https://blog.csdn.net/weixin_38553453/article/details/72921797

https://blog.csdn.net/LightOfMiracle/article/details/73456832

https://www.cnblogs.com/baizhanshi/p/6425209.html

一道百度java面试题的多种解法的更多相关文章

  1. 通往大神之路,百度Java面试题前200页。

    基本概念 操作系统中 heap 和 stack 的区别 什么是基于注解的切面实现 什么是 对象/关系 映射集成模块 什么是 Java 的反射机制 什么是 ACID BS与CS的联系与区别 Cookie ...

  2. Java高级面试题解析(二):百度Java面试题前200页(精选)

    基本概念 操作系统中 heap 和 stack 的区别 heap是堆,stack是栈,是两种不同的数据结构.堆是队列优先,先进先出:栈是先进后出. 在java多线程中,每个线程都有自己的栈:不同的线程 ...

  3. 百度java开发面试题

    第一面  项目:  1.找一个项目,介绍下情况.其中遇到了什么问题,每种问题怎么样的解决方案.  算法题:  2.一个排好序的数组,找出两数之和为m的所有组合  3.自然数序列,找出任意连续之和等于n ...

  4. 百度搜索 “Java面试题” 前200页(面试必看)

    前言 本文中的题目来源于网上的一篇文章<百度搜索 "Java面试题" 前200页>,但该文章里面只有题目,没有答案.因此,我整理了一些答案发布于本文.本文整理答案的原则 ...

  5. 一道简单的面试题,难倒各大 Java 高手!

    Java技术栈 www.javastack.cn 优秀的Java技术公众号 最近栈长在我们的<Java技术栈知识星球>上分享的一道 Java 实战面试题,很有意思,现在拿出来和大家分享下, ...

  6. 一道非常棘手的 Java 面试题:i++ 是线程安全的吗

    转载自  一道非常棘手的 Java 面试题:i++ 是线程安全的吗 i++ 是线程安全的吗? 相信很多中高级的 Java 面试者都遇到过这个问题,很多对这个不是很清楚的肯定是一脸蒙逼.内心肯定还在质疑 ...

  7. 一道关于java 类初始化 成员初始化的笔试题的解析

    代码如下: java笔试题public class Mapplication { private static int n; private static Mapplication m1 = new ...

  8. 2019百度阿里Java面试题(基础+框架+数据库+分布式+JVM+多线程)

    前言 很多朋友对面试不够了解,不知道如何准备,对面试环节的设置以及目的不够了解,因此成功率不高.通常情况下校招生面试的成功率低于1%,而社招的面试成功率也低于5%,所以对于候选人一定要知道设立面试的初 ...

  9. 面试百度、阿里、腾讯,这134道Java面试题你会多少?

    这里一共是134道Java面试题,看看你能对几道吧! 1. Java 语言有哪些特点 2. 面向对象和面向过程的区别 3. 关于 JVM JDK 和 JRE 最详细通俗的解答 4. Oracle JD ...

随机推荐

  1. Java学习的第三十一天

    1.使用RandomAccessFile随机读写文件 2.没有问题 3.明天学习综合实例

  2. Mybatis的dao层实现 接口代理方式实现规范+plugins-PageHelper

    Mybatis的dao层实现 接口代理方式实现规范 Mapper接口实现时的相关规范: Mapper接口开发只需要程序员编写Mapper接口而不用具体实现其代码(相当于我们写的Imp实现类) Mapp ...

  3. 水题挑战4: luogu P1280 尼克的任务

    题目描述 尼克每天上班之前都连接上英特网,接收他的上司发来的邮件,这些邮件包含了尼克主管的部门当天要完成的全部任务,每个任务由一个开始时刻与一个持续时间构成. 尼克的一个工作日为 \(n\) 分钟,从 ...

  4. Fira Code字体安装与配置

    俗话说,工欲善其事,必先利其器.算法固然重要,但真正实践也很重要. 一个字体的好看程度,直接决定了写代码和看代码的心情.比如这样: 代码1: #include <iostream> #in ...

  5. Luogu P2024 [NOI2001]食物链

    并查集 首先先要读懂题目,a是b的食物的话,b的天敌是a,b的食物是a的天敌 比如,人吃鸡,鸡吃草,那么草吃人..... 所以建3个并查集,+n时表示这是其食物,+2*n时表示这是其天敌 所以当x,y ...

  6. HTML+JavaScript实现一个简单抽奖功能

    为什么会做这个东西呢,纯属好玩,闲的其实是在上次班会的时候想到的,班会的时候叫人回答问题,没人回答当时就想,我如果抽签抽到你了,你还是不回答吗??好吧,一切都是扯淡先来看看页面效果吧:点击抽取就可以抽 ...

  7. 内网渗透 day2-nmap和nc的使用

    nmap和nc的使用 nmap的使用 1. nmap -sSV 172.16.100.214 -T4 -F -sS进行SYN扫描,是比较隐匿的 -sV探测打开端口的服务的信息 -sSV将上面两种一起使 ...

  8. TTL门和MOS门悬空输入的处理

    引言 本来是数字电路学习时很重要的考点,但是总容易忘掉,所以记录一下~ 内容 TTL TTL电路中的TTL是Transistor-Transistor-Logic的英文缩写,指的是晶体管逻辑电路,即T ...

  9. mybatis insert转update,duplicate关键字的使用示例,及返回情况说明

    主键存在时又insert转为update某个关键字段,示例如下,注意,如果这条数据曾经不存在,此时执行insert返回条目是1,如果已存在,执行update返回条目是2!!!<insert id ...

  10. 在线调整ceph的参数

    能够动态的进行系统参数的调整是一个很重要并且有用的属性 ceph的集群提供两种方式的调整,使用tell的方式和daemon设置的方式 一.tell方式设置 调整配置使用命令: 调整mon的参数 #ce ...