你会这道阿里多线程面试题吗?
背景
在前几天,群里有个群友问了我一道面试阿里的时候遇到的多线程题目,这个题目比较有意思,在这里和大家分享一下。
废话不多说,直接上题目:
通过N个线程顺序循环打印从0至100,如给定N=3则输出:
thread0: 0
thread1: 1
thread2: 2
thread0: 3
thread1: 4
.....
一些经常刷面试题的朋友,之前肯定遇到过下面这个题目:
两个线程交替打印0~100的奇偶数:
偶线程:0
奇线程:1
偶线程:2
奇线程:3
这两个题目看起来相似,第二个题目稍微来说比较简单一点,大家可以先思考一下两个线程奇偶数如何打印。
两线程奇偶数打印
有一些人这里可能会用讨巧的,用一个线程进行循环,在每次循环里面都会做是奇数还是偶数的判断,然后打印出这个我们想要的结果。在这里我们不过多讨论这种违背题目本意的做法。
其实要做这个题目我们就需要控制两个线程的执行顺序,偶线程执行完之后奇数线程执行,这个有点像通知机制,偶线程通知奇线程,奇线程再通知偶线程。而一看到通知/等待,立马就有朋友想到了Object中的wait和notify。没错,这里我们用wait和notify对其进行实现,代码如下:
public class 交替打印奇偶数 {
static class SoulutionTask implements Runnable{
static int value = 0;
@Override
public void run() {
while (value <= 100){
synchronized (SoulutionTask.class){
System.out.println(Thread.currentThread().getName() + ":" + value++);
SoulutionTask.class.notify();
try {
SoulutionTask.class.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
public static void main(String[] args) {
new Thread(new SoulutionTask(), "偶数").start();
new Thread(new SoulutionTask(), "奇数").start();
}
}
这里我们有两个线程,通过notify和wait用来控制我们线程的执行,从而打印出我们目标的结果
N个线程循环打印
再回到我们最初的问题来,N个线程进行循环打印,这个问题我再帮助群友解答了之后,又再次把这个问题在群里面抛了出来,不少老司机之前看过交替打印奇偶数这道题目,于是马上做出了几个版本,让我们看看老司机1的代码:
public class 老司机1 implements Runnable {
private static final Object LOCK = new Object();
/**
* 当前即将打印的数字
*/
private static int current = 0;
/**
* 当前线程编号,从0开始
*/
private int threadNo;
/**
* 线程数量
*/
private int threadCount;
/**
* 打印的最大数值
*/
private int maxInt;
public 老司机1(int threadNo, int threadCount, int maxInt) {
this.threadNo = threadNo;
this.threadCount = threadCount;
this.maxInt = maxInt;
}
@Override
public void run() {
while (true) {
synchronized (LOCK) {
// 判断是否轮到当前线程执行
while (current % threadCount != threadNo) {
if (current > maxInt) {
break;
}
try {
// 如果不是,则当前线程进入wait
LOCK.wait();
} catch (Exception e) {
e.printStackTrace();
}
}
// 最大值跳出循环
if (current > maxInt) {
break;
}
System.out.println("thread" + threadNo + " : " + current);
current++;
// 唤醒其他wait线程
LOCK.notifyAll();
}
}
}
public static void main(String[] args) {
int threadCount = 3;
int max = 100;
for (int i = 0; i < threadCount; i++) {
new Thread(new 老司机1(i, threadCount, max)).start();
}
}
}
核心方法在run里面,可以看见和我们交替打印奇偶数原理差不多,这里将我们的notify改成了notifyAll,这里要注意一下很多人会将notifyAll理解成其他wait的线程全部都会执行,其实是错误的。这里只会将wait的线程解除当前wait状态,也叫作唤醒,由于我们这里用同步锁synchronized块包裹住,那么唤醒的线程会做会抢夺同步锁。
这个老司机的代码的确能跑通,但是有一个问题是什么呢?当我们线程数很大的时候,由于我们不确定唤醒的线程到底是否是下一个要执行的就有可能会出现抢到了锁但不该自己执行,然后又进入wait的情况,比如现在有100个线程,现在是第一个线程在执行,他执行完之后需要第二个线程执行,但是第100个线程抢到了,发现不是自己然后又进入wait,然后第99个线程抢到了,发现不是自己然后又进入wait,然后第98,97...直到第3个线程都抢到了,最后才到第二个线程抢到同步锁,这里就会白白的多执行很多过程,虽然最后能完成目标。
还有其他老司机用lock/condition也实现了这样的功能,还有老司机用比较新颖的方法比如队列去做,当然这里就不多提了,大致的原理都是基于上面的,这里我说一下我的做法,在Java的多线程中提供了一些常用的同步器,在这个场景下比较适合于使用Semaphore,也就是信号量,我们上一个线程持有下一个线程的信号量,通过一个信号量数组将全部关联起来,代码如下:
static int result = 0;
public static void main(String[] args) throws InterruptedException {
int N = 3;
Thread[] threads = new Thread[N];
final Semaphore[] syncObjects = new Semaphore[N];
for (int i = 0; i < N; i++) {
syncObjects[i] = new Semaphore(1);
if (i != N-1){
syncObjects[i].acquire();
}
}
for (int i = 0; i < N; i++) {
final Semaphore lastSemphore = i == 0 ? syncObjects[N - 1] : syncObjects[i - 1];
final Semaphore curSemphore = syncObjects[i];
final int index = i;
threads[i] = new Thread(new Runnable() {
public void run() {
try {
while (true) {
lastSemphore.acquire();
System.out.println("thread" + index + ": " + result++);
if (result > 100){
System.exit(0);
}
curSemphore.release();
}
} catch (Exception e) {
e.printStackTrace();
}
}
});
threads[i].start();
}
}
通过这种方式,我们就不会有白白唤醒的线程,每一个线程都按照我们所约定的顺序去执行,这其实也是面试官所需要考的地方,让每个线程的执行都能再你手中得到控制,这也可以验证你多线程知识是否牢固。
最后这篇文章被我收录于JGrowing-Java面试篇,一个全面,优秀,由社区一起共建的Java学习路线,如果您想参与开源项目的维护,可以一起共建,github地址为:https://github.com/javagrowing/JGrowing 麻烦给个小星星哟。
如果大家觉得这篇文章对你有帮助,你的关注和转发是对我最大的支持,O(∩_∩)O:

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
-
上一篇
从零开始社区之路,手把手教你开源自己的Python包
要融入社区,第一步当然是要撰写一个自己的包。整个过程主要分为五步: 项目创建 搭建虚拟运行环境 编写项目代码 编写安装脚本 上传PyPi GIT 创建项目 创建项目,确定项目名称,description, license等: 项目地址:https://github.com/shikanon/BaiduMapAPI 搭建虚拟环境 我们在搭建自己的库的时候,是希望有一个干净的项目环境的,这时候virtualenv就很有用了,采用 virtualev 搭建虚拟环境,可以方便为后面生成私有项目的 requirement.txt 依赖包文件。 创建虚拟环境 virtualev venv 启用 virtualev : source venv/Script/activate 构建项目代码 简单,快速构建框架原型和骨架,记得包之间需要 __init__.py 文件,后面在编写setup.py也会很有用。 项目结构: 构建好架构后,可以开始编写单元测试代码,pytest是个简单易用的库,可以帮助我们快速完成单元测试构建。 构建安装脚本,编写 setup.py 文件 完成代码构建和测试就可以开始进入构建安...
-
下一篇
深入解析Node.js Stream ondata触发时机与顺序
今天小编就为大家分享一篇关于Node.js Stream ondata触发时机与顺序的探索,写的十分的全面细致,具有一定的参考价值,对此有需要的朋友可以参考学习下。如有不足之处,欢迎批评指正。 无用逻辑 当时研究pipe细节是基于Node.js v8.11.1的源码,其中针对上游的ondata事件处理有如下一段代码: // If the user pushes more data while we're writing to dest then we'll end up // in ondata again. However, we only want to increase awaitDrain once because // dest will only emit one 'drain' event for the multiple writes. // => Introduce a guard on increasing awaitDrain. var increasedAwaitDrain = false; src.on('data', ondata); function...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- CentOS7,8上快速安装Gitea,搭建Git服务器
- MySQL数据库在高并发下的优化方案
- Linux系统CentOS6、CentOS7手动修改IP地址
- Windows10,CentOS7,CentOS8安装Nodejs环境
- Hadoop3单机部署,实现最简伪集群
- CentOS8安装MyCat,轻松搞定数据库的读写分离、垂直分库、水平分库
- SpringBoot2编写第一个Controller,响应你的http请求并返回结果
- SpringBoot2整合Redis,开启缓存,提高访问速度
- Docker快速安装Oracle11G,搭建oracle11g学习环境
- Springboot2将连接池hikari替换为druid,体验最强大的数据库连接池