一道百度的java面试题的几种解法
考试结束,班级平均分只拿到了年级第二,班主任于是问道:大家都知道世界第一高峰珠穆朗玛峰,有人知道世界第二高峰是什么吗?正当班主任要继续发话,只听到角落默默想起来一个声音:”乔戈里峰”
前言
文章出自:https://www.cnblogs.com/dudu19939/p/9784116.html
这道题是群里的朋友的投稿,上面是这位朋友的原文博客链接,这道题目我在百度的面试也遇到过,当时这位朋友在这个问题的博客也与我交流过,我也贡献了其中一种方法,嘿嘿,最后看了这位朋友的成文,觉得写得很不错,望与诸君共勉(PS:欢迎大家投稿)。
题目
下面是我在2018年10月11日二面百度的时候的一个问题:
java程序,主进程需要等待多个子进程结束之后再执行后续的代码,有哪些方案可以实现?
这个需求其实我们在工作中经常会用到,比如用户下单一个产品,后台会做一系列的处理,为了提高效率,每个处理都可以用一个线程来执行,所有处理完成了之后才会返回给用户下单成功,欢迎大家批评指正。
解法
1.join方法
使用Thread的join()等待所有的子线程执行完毕,主线程在执行,thread.join()把指定的线程加入到当前线程,可以将两个交替执行的线程合并为顺序执行的线程。比如在线程B中调用了线程A的join()方法,直到线程A执行完毕后,才会继续执行线程B。
import java.util.Vector;
public class Test {
public static void main(String[] args) throws InterruptedException {
Vector<Thread> vector = new Vector<>();
for(int i=0;i<5;i++) {
Thread childThread= new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("子线程被执行");
}
});
vector.add(childThread);
childThread.start();
}
for(Thread thread : vector) {
thread.join();
}
System.out.println("主线程被执行");
}
执行结果
子线程被执行
子线程被执行
子线程被执行
子线程被执行
子线程被执行
主线程被执行
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使用完毕后,它不能再次被使用。
import java.util.Vector;
import java.util.concurrent.CountDownLatch;
public class Test2 {
public static void main(String[] args) throws InterruptedException {
final CountDownLatch latch = new CountDownLatch(5);
for(int i=0;i<5;i++) {
Thread childThread= new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("子线程被执行");
latch.countDown();
}
});
childThread.start();
}
latch.await();//阻塞当前线程直到latch中的值
System.out.println("主线程被执行");
}
}
执行结果:
子线程被执行
子线程被执行
子线程被执行
子线程被执行
子线程被执行
主线程被执行
3.同步屏障CyclicBarrier
这里必须注意,CylicBarrier是控制一组线程的同步,初始化的参数:5的含义是包括主线程在内有5个线程,所以只能有四个子线程,这与CountDownLatch是不一样的。
countDownLatch和cyclicBarrier有什么区别呢,他们的区别:countDownLatch只能使用一次,而CyclicBarrier方法可以使用reset()方法重置,所以CyclicBarrier方法可以能处理更为复杂的业务场景。
我曾经在网上看到一个关于countDownLatch和cyclicBarrier的形象比喻,就是在百米赛跑的比赛中若使用 countDownLatch的话冲过终点线一个人就给评委发送一个人的成绩,10个人比赛发送10次,如果用CyclicBarrier,则只在最后一个人冲过终点线的时候发送所有人的数据,仅仅发送一次,这就是区别。
package interview;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
public class Test3 {
public static void main(String[] args) throws InterruptedException, BrokenBarrierException {
final CyclicBarrier barrier = new CyclicBarrier(5);
for(int i=0;i<4;i++) {
Thread childThread= new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("子线程被执行");
try {
barrier.await();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (BrokenBarrierException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
});
childThread.start();
}
barrier.await();//阻塞当前线程直到latch中的值
System.out.println("主线程被执行");
}
}
执行结果:
子线程被执行
子线程被执行
子线程被执行
子线程被执行
主线程被执行
4.使用yield方法(注意此种方法经过亲自试验证明并不可靠!)
public class Test4 {
public static void main(String[] args) throws InterruptedException {
for(int i=0;i<5;i++) {
Thread childThread= new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("子线程被执行");
}
});
childThread.start();
}
while (Thread.activeCount() > 2) { //保证前面的线程都执行完
Thread.yield();
}
System.out.println("主线程被执行");
}
}
执行结果:
子线程被执行
子线程被执行
子线程被执行
子线程被执行
主线程被执行
子线程被执行
为何yield方法会出现这样的问题?
使当前线程从执行状态(运行状态)变为可执行态(就绪状态)。cpu会从众多的可执行态里选择,也就是说,当前也就是刚刚的那个线程还是有可能会被再次执行到的,并不是说一定会执行其他线程而该线程在下一次中不会执行到了。
Java线程中有一个Thread.yield( )方法,很多人翻译成线程让步。顾名思义,就是说当一个线程使用了这个方法之后,它就会把自己CPU执行的时间让掉,让自己或者其它的线程运行。
打个比方:现在有很多人在排队上厕所,好不容易轮到这个人上厕所了,突然这个人说:“我要和大家来个竞赛,看谁先抢到厕所!”,然后所有的人在同一起跑线冲向厕所,有可能是别人抢到了,也有可能他自己有抢到了。我们还知道线程有个优先级的问题,那么手里有优先权的这些人就一定能抢到厕所的位置吗? 不一定的,他们只是概率上大些,也有可能没特权的抢到了。
yield的本质是把当前线程重新置入抢CPU时间的”队列”(队列只是说所有线程都在一个起跑线上.并非真正意义上的队列)。
5.FutureTast可用于闭锁,类似于CountDownLatch的作用
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class Test5 {
public static void main(String[] args) {
MyThread td = new MyThread();
//1.执行 Callable 方式,需要 FutureTask 实现类的支持,用于接收运算结果。
FutureTask<Integer> result1 = new FutureTask<>(td);
new Thread(result1).start();
FutureTask<Integer> result2 = new FutureTask<>(td);
new Thread(result2).start();
FutureTask<Integer> result3 = new FutureTask<>(td);
new Thread(result3).start();
Integer sum;
try {
sum = result1.get();
sum = result2.get();
sum = result3.get();
//这里获取三个sum值只是为了同步,并没有实际意义
System.out.println(sum);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (ExecutionException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} //FutureTask 可用于 闭锁 类似于CountDownLatch的作用,在所有的线程没有执行完成之后这里是不会执行的
System.out.println("主线程被执行");
}
}
class MyThread implements Callable<Integer> {
@Override
public Integer call() throws Exception {
int sum = 0;
Thread.sleep(1000);
for (int i = 0; i <= 10; i++) {
sum += i;
}
System.out.println("子线程被执行");
return sum;
}
}
6.使用callable+future
Callable+Future最终也是以Callable+FutureTask的形式实现的。
在这种方式中调用了: Future future = executor.submit(task);
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class Test6 {
public static void main(String[] args) throws InterruptedException, ExecutionException {
ExecutorService executor = Executors.newCachedThreadPool();
Task task = new Task();
Future<Integer> future1 = executor.submit(task);
Future<Integer> future2 = executor.submit(task);
//获取线程执行结果,用来同步
Integer result1 = future1.get();
Integer result2 = future2.get();
System.out.println("主线程执行");
executor.shutdown();
}
}
class Task implements Callable<Integer>{
@Override public Integer call() throws Exception {
int sum = 0;
//do something;
System.out.println("子线程被执行");
return sum;
}
}
执行结果:
子线程被执行
子线程被执行
主线程执行
补充:
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
作者乔戈里亲历2019秋招,哈工大计算机本硕,百度准入职java工程师,欢迎大家关注我的微信公众号:程序员乔戈里,公众号有3T编程资源,以及我和我朋友(准入职百度C++工程师)在秋招期间整理的近200M的面试必考的java与C++面经,并有每天一道leetcode打卡群与技术交流群,欢迎关注。
一道百度的java面试题的几种解法的更多相关文章
- 百度搜索 “Java面试题” 前200页(面试必看)
前言 本文中的题目来源于网上的一篇文章<百度搜索 "Java面试题" 前200页>,但该文章里面只有题目,没有答案.因此,我整理了一些答案发布于本文.本文整理答案的原则 ...
- OpenJDK源码研究笔记(五)-缓存Integer等类型的频繁使用的数据和对象,大幅度提升性能(一道经典的Java笔试题)
摘要 本文先给出一个看似很简单实则有深意的Java笔试面试题,引出JDK内部的缓存. JDK内部的缓存,主要是为了提高Java程序的性能. 你能答对这道"看似简单,实则有深意"的J ...
- 2019百度阿里Java面试题(基础+框架+数据库+分布式+JVM+多线程)
前言 很多朋友对面试不够了解,不知道如何准备,对面试环节的设置以及目的不够了解,因此成功率不高.通常情况下校招生面试的成功率低于1%,而社招的面试成功率也低于5%,所以对于候选人一定要知道设立面试的初 ...
- Shell在大数据时代的魅力:从一道百度大数据面试题想到的点滴
对于在Linux下开发的同学来说,Shell可以说是一种基本功. 对于运维的同学来说,Shell可以说是一种必备的技能,而且应该要非常熟练的书写Shell.对于Release Team,软件配置管理的 ...
- 一道简单的 Java 笔试题,但值得很多人反思!
前言 面试别人,对我来说是一件新奇事,以前都是别人面试我.我清楚地知道,我在的地域与公司,难以吸引到中国的一流软件人才.所以,我特地调低了期望,很少问什么深入的技术问题,只问一些广泛的.基础的.我只要 ...
- 遇到一道经典的java面试题
在文件 nameList.txt 中按下图格式存放着超过50万人的姓名与身份证信息.请使用您熟悉的编程语言(Java或C/C++)编写一段程序, 将出生日期落在1995年1月1日与1999年12月31 ...
- 70道阿里百度高频Java面试题(框架+JVM+多线程+算法+数据库)
基础与框架 1.String类能被继承吗,为什么 2.String,Stringbuffer,StringBuilder的区别? 3.ArrayList和LinkedList有什么区别 4.类的实例化 ...
- 脉脉的一道网红Java面试题
题目如下: public class Test { public static void main(String[] args) { int a = 10; int b = 10; // 需要在met ...
- 一道经典的Java面试题:equals ,== 和hashcode()的区别
一句话区别:==比较的是内存地址,equals比较的是值内容 结论: 从以下三点展开: 基本数据类型.字符串.对象 对于基本数据类型:只有==,没有equals. 对于字符串:==比较的是内存地址,e ...
随机推荐
- C#实现像微信PC版一样的扫码登录功能
现在好些网站都支持扫码登录,感觉上安全了很多,但是本地程序扫码登录的不多,就用C#实现了一下,需要作如下准备 在官网上申请一个企业微信,有条件的话做个企业认证吧,我们的是认证过的,所以账号和本地其他系 ...
- Pi 在Windows下面使用远程桌面登录
1.删除系统自带的xrdp 输入命令sudo apt-get purge xrdp pi@raspberrypi:~ $ sudo apt-get purge xrdp 正在读取软件包列表... 完成 ...
- WPF自定义ComboBox
<ControlTemplate x:Key="ComboBoxTextBox" TargetType="{x:Type TextBox}"> &l ...
- 总结目前为止学到的关键字(break,continue,private,static,this,super,final,abstract)
1.控制跳转语句:break(结束) 使用的场景: a.循环当中 b.switch break关键字需要注意的问题: 1.break关键字只能用于循环和switch语句当中,其本质就是结束整段语句的意 ...
- golang plugin的依赖问题
golang plugin的依赖问题 此文中涉及的plugin运行环境为mac 10.14,go版本为1.11 主要是想讨论一下插件依赖的第三方库的问题. 例子是在https://github.com ...
- “全栈2019”Java异常第七章:try-catch-finally组合方式
难度 初级 学习时间 10分钟 适合人群 零基础 开发语言 Java 开发环境 JDK v11 IntelliJ IDEA v2018.3 文章原文链接 "全栈2019"Java异 ...
- docker设置引用国内镜像加速
设置步骤: 1 先到daocloud.io网站注册一个账号 过程略,注册成功后,进入控制台 2 点击控制台上的加速器 拉到中间部分,有一个『主机监控程序』的文字链接,见下图: 然后选择主机类型,我用的 ...
- linux系统解决boot空间不足
有时候更新Linux系统是会碰到boot空间不足的错误,原因基本上是安装时boot空间设置问题可以通过删除旧的内核来释放boot空间. ubuntu: 1.查看当前使用内核版本号 unam ...
- Swift 函数提前返回
简评:函数提前返回主要的好处是:将每个错误处理进行分离,审查代码时不需要考虑多种复杂异常,我们可以吧注意力集中在也业务逻辑中,调试代码时可以直接在异常中打断点. 提前返回 首先来看一下需要改进的代码示 ...
- java日期时间
字母 日期或时间元素 表示 示例 G Era 标志符 Text AD y 年 Year 1996; 96 M 年中的月份 Month July; Jul; 07 w 年中的周数 Number 27 ...