阿里最喜欢问的多线程顺序打印的5种解法!

Keeper导读

大家在换工作面试中,除了一些常规算法题,还会遇到各种需要手写的题目,所以打算总结出来,给大家个参考。

全文 2929 字,剩下的是代码,P6 及以下阅读只需要 8 分钟,高 P 请直接关闭

第一篇打算总结下阿里最喜欢问的多个线程顺序打印问题,我遇到的是机试,直接写出运行。同类型的题目有很多,比如:

  1. 三个线程分别打印 A,B,C,要求这三个线程一起运行,打印 n 次,输出形如“ABCABCABC....”的字符串

  2. 两个线程交替打印 0~100 的奇偶数

  3. 通过 N 个线程顺序循环打印从 0 至 100

  4. 多线程按顺序调用,A->B->C,AA 打印 5 次,BB 打印10 次,CC 打印 15 次,重复 10 次

  5. 用两个线程,一个输出字母,一个输出数字,交替输出 1A2B3C4D...26Z

其实这类题目考察的都是线程间的通信问题,基于这类题目,做一个整理,方便日后手撕面试官,文明的打工人,手撕面试题。

1.使用 Lock

我们以第一题为例:三个线程分别打印 A,B,C,要求这三个线程一起运行,打印 n 次,输出形如“ABCABCABC....”的字符串。

思路:使用一个取模的判断逻辑 C%M ==N,题为 3 个线程,所以可以按取模结果编号:0、1、2,他们与 3 取模结果仍为本身,则执行打印逻辑。

public class PrintABCUsingLock {private int times; // 控制打印次数private int state;   // 当前状态值:保证三个线程之间交替打印private Lock lock = new ReentrantLock();public PrintABCUsingLock(int times) {this.times = times;}private void printLetter(String name, int targetNum) {for (int i = 0; i < times; ) {lock.lock();if (state % 3 == targetNum) {state++;i++;System.out.print(name);}lock.unlock();}}public static void main(String[] args) {PrintABCUsingLock loopThread = new PrintABCUsingLock(1);new Thread(() -> {loopThread.printLetter("B", 1);}, "B").start();new Thread(() -> {loopThread.printLetter("A", 0);}, "A").start();new Thread(() -> {loopThread.printLetter("C", 2);}, "C").start();}
}

main 方法启动后,3 个线程会抢锁,但是 state 的初始值为 0,所以第一次执行 if  语句的内容只能是 线程 A,然后还在 for 循环之内,此时 state = 1,只有 线程 B 才满足 1% 3 == 1,所以第二个执行的是 B,同理只有 线程 C 才满足 2% 3 == 2,所以第三个执行的是 C,执行完 ABC 之后,才去执行第二次 for 循环,所以要把 i++ 写在 for 循环里边,不能写成 for (int i = 0; i < times;i++)  这样。

2.使用 wait/notify

其实遇到这类型题目,好多同学可能会先想到的就是 join(),或者 wati/notify 这样的思路。算是比较传统且万能的解决方案。也有些面试官会要求不能使用这种方式。

思路:还是以第一题为例,我们用对象监视器来实现,通过 waitnotify() 方法来实现等待、通知的逻辑,A 执行后,唤醒 B,B 执行后唤醒 C,C 执行后再唤醒 A,这样循环的等待、唤醒来达到目的。

public class PrintABCUsingWaitNotify {private int state;private int times;private static final Object LOCK = new Object();public PrintABCUsingWaitNotify(int times) {this.times = times;}public static void main(String[] args) {PrintABCUsingWaitNotify printABC = new PrintABCUsingWaitNotify(10);new Thread(() -> {printABC.printLetter("A", 0);}, "A").start();new Thread(() -> {printABC.printLetter("B", 1);}, "B").start();new Thread(() -> {printABC.printLetter("C", 2);}, "C").start();}private void printLetter(String name, int targetState) {for (int i = 0; i < times; i++) {synchronized (LOCK) {while (state % 3 != targetState) {try {LOCK.wait();} catch (InterruptedException e) {e.printStackTrace();}}state++;System.out.print(name);LOCK.notifyAll();}}}
}

同样的思路,来解决下第 2 题:两个线程交替打印奇数和偶数

使用对象监视器实现,两个线程 A、B 竞争同一把锁,只要其中一个线程获取锁成功,就打印 ++i,并通知另一线程从等待集合中释放,然后自身线程加入等待集合并释放锁即可。

图:throwable-blog
public class OddEvenPrinter {private Object monitor = new Object();private final int limit;private volatile int count;OddEvenPrinter(int initCount, int times) {this.count = initCount;this.limit = times;}public static void main(String[] args) {OddEvenPrinter printer = new OddEvenPrinter(0, 10);new Thread(printer::print, "odd").start();new Thread(printer::print, "even").start();}private void print() {synchronized (monitor) {while (count < limit) {try {System.out.println(String.format("线程[%s]打印数字:%d", Thread.currentThread().getName(), ++count));monitor.notifyAll();monitor.wait();} catch (InterruptedException e) {e.printStackTrace();}}//防止有子线程被阻塞未被唤醒,导致主线程不退出monitor.notifyAll();}}
}

同样的思路,来解决下第 5 题:用两个线程,一个输出字母,一个输出数字,交替输出 1A2B3C4D...26Z

public class NumAndLetterPrinter {private static char c = 'A';private static int i = 0;static final Object lock = new Object();public static void main(String[] args) {new Thread(() -> printer(), "numThread").start();new Thread(() -> printer(), "letterThread").start();}private static void printer() {synchronized (lock) {for (int i = 0; i < 26; i++) {if (Thread.currentThread().getName() == "numThread") {//打印数字1-26System.out.print((i + 1));// 唤醒其他在等待的线程lock.notifyAll();try {// 让当前线程释放锁资源,进入wait状态lock.wait();} catch (InterruptedException e) {e.printStackTrace();}} else if (Thread.currentThread().getName() == "letterThread") {// 打印字母A-ZSystem.out.print((char) ('A' + i));// 唤醒其他在等待的线程lock.notifyAll();try {// 让当前线程释放锁资源,进入wait状态lock.wait();} catch (InterruptedException e) {e.printStackTrace();}}}lock.notifyAll();}}
}

3.使用 Lock/Condition

还是以第一题为例,使用 Condition 来实现,其实和 wait/notify 的思路一样。

Condition 中的 await() 方法相当于 Object 的 wait() 方法,Condition 中的 signal() 方法相当于Object 的 notify() 方法,Condition 中的 signalAll() 相当于 Object 的 notifyAll() 方法。

不同的是,Object 中的 wait(),notify(),notifyAll()方法是和"同步锁"(synchronized关键字)捆绑使用的;而 Condition 是需要与"互斥锁"/"共享锁"捆绑使用的。

public class PrintABCUsingLockCondition {private int times;private int state;private static Lock lock = new ReentrantLock();private static Condition c1 = lock.newCondition();private static Condition c2 = lock.newCondition();private static Condition c3 = lock.newCondition();public PrintABCUsingLockCondition(int times) {this.times = times;}public static void main(String[] args) {PrintABCUsingLockCondition print = new PrintABCUsingLockCondition(10);new Thread(() -> {print.printLetter("A", 0, c1, c2);}, "A").start();new Thread(() -> {print.printLetter("B", 1, c2, c3);}, "B").start();new Thread(() -> {print.printLetter("C", 2, c3, c1);}, "C").start();}private void printLetter(String name, int targetState, Condition current, Condition next) {for (int i = 0; i < times; ) {lock.lock();try {while (state % 3 != targetState) {current.await();}state++;i++;System.out.print(name);next.signal();} catch (Exception e) {e.printStackTrace();} finally {lock.unlock();}}}
}

使用 Lock 锁的多个 Condition 可以实现精准唤醒,所以碰到那种多个线程交替打印不同次数的题就比较容易想到,比如解决第四题:多线程按顺序调用,A->B->C,AA 打印 5 次,BB 打印10 次,CC 打印 15 次,重复 10 次

代码就不贴了,思路相同。

以上几种方式,其实都会存在一个锁的抢夺过程,如果抢锁的的线程数量足够大,就会出现很多线程抢到了锁但不该自己执行,然后就又解锁或 wait() 这种操作,这样其实是有些浪费资源的。

4.使用 Semaphore

在信号量上我们定义两种操作:信号量主要用于两个目的,一个是用于多个共享资源的互斥使用,另一个用于并发线程数的控制。

  1. acquire(获取) 当一个线程调用 acquire 操作时,它要么通过成功获取信号量(信号量减1),要么一直等下去,直到有线程释放信号量,或超时。

  2. release(释放)实际上会将信号量的值加1,然后唤醒等待的线程。

先看下如何解决第一题:三个线程循环打印 A,B,C

public class PrintABCUsingSemaphore {private int times;private static Semaphore semaphoreA = new Semaphore(1); // 只有A 初始信号量为1,第一次获取到的只能是Aprivate static Semaphore semaphoreB = new Semaphore(0);private static Semaphore semaphoreC = new Semaphore(0);public PrintABCUsingSemaphore(int times) {this.times = times;}public static void main(String[] args) {PrintABCUsingSemaphore printer = new PrintABCUsingSemaphore(1);new Thread(() -> {printer.print("A", semaphoreA, semaphoreB);}, "A").start();new Thread(() -> {printer.print("B", semaphoreB, semaphoreC);}, "B").start();new Thread(() -> {printer.print("C", semaphoreC, semaphoreA);}, "C").start();}private void print(String name, Semaphore current, Semaphore next) {for (int i = 0; i < times; i++) {try {System.out.println("111" + Thread.currentThread().getName());current.acquire();  // A获取信号执行,A信号量减1,当A为0时将无法继续获得该信号量System.out.print(name);next.release();    // B释放信号,B信号量加1(初始为0),此时可以获取B信号量System.out.println("222" + Thread.currentThread().getName());} catch (InterruptedException e) {e.printStackTrace();}}}
}

如果题目中是多个线程循环打印的话,一般使用信号量解决是效率较高的方案,上一个线程持有下一个线程的信号量,通过一个信号量数组将全部关联起来,这种方式不会存在浪费资源的情况。

接着用信号量的方式解决下第三题:通过 N 个线程顺序循环打印从 0 至 100

public class LoopPrinter {private final static int THREAD_COUNT = 3;static int result = 0;static int maxNum = 10;public static void main(String[] args) throws InterruptedException {final Semaphore[] semaphores = new Semaphore[THREAD_COUNT];for (int i = 0; i < THREAD_COUNT; i++) {//非公平信号量,每个信号量初始计数都为1semaphores[i] = new Semaphore(1);if (i != THREAD_COUNT - 1) {System.out.println(i+"==="+semaphores[i].getQueueLength());//获取一个许可前线程将一直阻塞, for 循环之后只有 syncObjects[2] 没有被阻塞semaphores[i].acquire();}}for (int i = 0; i < THREAD_COUNT; i++) {// 初次执行,上一个信号量是 syncObjects[2]final Semaphore lastSemphore = i == 0 ? semaphores[THREAD_COUNT - 1] : semaphores[i - 1];final Semaphore currentSemphore = semaphores[i];final int index = i;new Thread(() -> {try {while (true) {// 初次执行,让第一个 for 循环没有阻塞的 syncObjects[2] 先获得令牌阻塞了lastSemphore.acquire();System.out.println("thread" + index + ": " + result++);if (result > maxNum) {System.exit(0);}// 释放当前的信号量,syncObjects[0] 信号量此时为 1,下次 for 循环中上一个信号量即为syncObjects[0]currentSemphore.release();}} catch (Exception e) {e.printStackTrace();}}).start();}}
}

5.使用 LockSupport

LockSupport 是 JDK 底层的基于 sun.misc.Unsafe 来实现的类,用来创建锁和其他同步工具类的基本线程阻塞原语。它的静态方法unpark()park()可以分别实现阻塞当前线程和唤醒指定线程的效果,所以用它解决这样的问题会更容易一些。

(在 AQS 中,就是通过调用 LockSupport.park( )LockSupport.unpark() 来实现线程的阻塞和唤醒的。)

public class PrintABCUsingLockSupport {private static Thread threadA, threadB, threadC;public static void main(String[] args) {threadA = new Thread(() -> {for (int i = 0; i < 10; i++) {// 打印当前线程名称System.out.print(Thread.currentThread().getName());// 唤醒下一个线程LockSupport.unpark(threadB);// 当前线程阻塞LockSupport.park();}}, "A");threadB = new Thread(() -> {for (int i = 0; i < 10; i++) {// 先阻塞等待被唤醒LockSupport.park();System.out.print(Thread.currentThread().getName());// 唤醒下一个线程LockSupport.unpark(threadC);}}, "B");threadC = new Thread(() -> {for (int i = 0; i < 10; i++) {// 先阻塞等待被唤醒LockSupport.park();System.out.print(Thread.currentThread().getName());// 唤醒下一个线程LockSupport.unpark(threadA);}}, "C");threadA.start();threadB.start();threadC.start();}
}

理解了思路,解决其他问题就容易太多了。

比如,我们再解决下第五题:用两个线程,一个输出字母,一个输出数字,交替输出 1A2B3C4D...26Z

public class NumAndLetterPrinter {private static Thread numThread, letterThread;public static void main(String[] args) {letterThread = new Thread(() -> {for (int i = 0; i < 26; i++) {System.out.print((char) ('A' + i));LockSupport.unpark(numThread);LockSupport.park();}}, "letterThread");numThread = new Thread(() -> {for (int i = 1; i <= 26; i++) {System.out.print(i);LockSupport.park();LockSupport.unpark(letterThread);}}, "numThread");numThread.start();letterThread.start();}
}

写在最后

好了,以上就是常用的五种实现方案,多练习几次,手撕没问题。

当然,这类问题,解决方式不止是我列出的这些,还会有 join、CountDownLatch、也有放在队列里解决的,思路有很多,面试官想考察的其实只是对多线程的编程功底,其实自己练习的时候,是个很好的巩固理解 JUC 的过程。


往期推荐

多图带你彻底理解Java中的21种锁!

2020-09-24

Java中的5大队列,你知道几个?

2020-10-24

队列实现栈的3种方法,全都击败了100%的用户!

2020-11-02


关注我,每天陪你进步一点点!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/545268.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

CSS入门

CSS入门1_CSS概要1.1_CSS引入方式2_CSS选择器3_字体样式3.1_字体属性3.2_字体类型&#xff1a;font-family3.3_字体大小&#xff1a;font-size3.4_字体粗细&#xff1a;font-weight3.5_字体颜色&#xff1a;color3.6_总结4_文本样式4.1_文本样式属性4.2_首行缩进&#xff1a;te…

23张图!万字详解「链表」,从小白到大佬!

作者 | 王磊来源 | Java中文社群&#xff08;ID&#xff1a;javacn666&#xff09;转载请联系授权&#xff08;微信ID&#xff1a;GG_Stone&#xff09;链表和数组是数据类型中两个重要又常用的基础数据类型。数组是连续存储在内存中的数据结构&#xff0c;因此它的优势是可以通…

何为“秒传”

写在前面 最近一直在弄文件传输的组件&#xff0c;在讨论组里面&#xff0c;有同事提到“秒传”的功能。在目前的通信软件中也有网盘的功能&#xff0c;就从网上搜了一下&#xff0c;这里对“秒传”的实现思路做一下总结&#xff0c;在之后会写一个小的demo实现一下。没有其他&…

数组转List的3种方法和使用对比!

作者 | 大脑补丁来源 | blog.csdn.net/x541211190/article/details/79597236前言&#xff1a;本文介绍Java中数组转为List三种情况的优劣对比&#xff0c;以及应用场景的对比&#xff0c;以及程序员常犯的类型转换错误原因解析。一.最常见方式&#xff08;未必最佳&#xff09;…

oracle10g备份导入

2019独角兽企业重金招聘Python工程师标准>>> //导出 exp test/testgdsoft filed:\gd.dmp //删用户 drop user wkwx cascade; //用PLSQL创建数据数据库 create user hzjzjn identified by hzjzjn default tablespace BDP_DATABD; grant CREATE USER,DROP USER,ALTE…

VisualSVNServer的使用

VisualSVNServer的使用1_服务端初识1.1_创建新仓库1.2_创建用户并分配权限1_服务端初识 1.1_创建新仓库 右击Repository&#xff0c;点击create 点击下一步 输入仓库名 右击空白处&#xff0c;点击新建&#xff0c;点击project structure 输入工程名 1.2_创建用户并分…

安利一个IDEA骚操作:一键生成方法的序列图

在平时的学习/工作中&#xff0c;我们会经常面临如下场景&#xff1a;阅读别人的代码阅读框架源码阅读自己很久之前写的代码。千万不要觉得工作就是单纯写代码&#xff0c;实际工作中&#xff0c;你会发现你的大部分时间实际都花在了阅读和理解已有代码上。为了能够更快更清晰地…

c#中textbox属性_C#.Net中带有示例的TextBox.Multiline属性

c#中textbox属性Here we are demonstrating use of Multiline property of the TextBox Control. 在这里&#xff0c;我们演示了TextBox控件的Multiline属性的使用。 Multiline property contains two values: 多行属性包含两个值&#xff1a; True: We can enter text in mo…

MATLAB新手教程

MATLAB新手教程 1&#xff0e;MATLAB的基本知识 1-1、基本运算与函数 在MATLAB下进行基本数学运算&#xff0c;仅仅需将运算式直接打入提示号&#xff08;>>&#xff09;之後&#xff0c;并按入Enter键就可以。比如&#xff1a; >> (5*21.3-0.8)*10/25 an…

嗯,查询滑动窗口最大值的这4种方法不错....

作者 | 王磊来源 | Java中文社群&#xff08;ID&#xff1a;javacn666&#xff09;转载请联系授权&#xff08;微信ID&#xff1a;GG_Stone&#xff09;本文已收录至 Github《小白学算法》系列&#xff1a;https://github.com/vipstone/algorithm这是一道比较基础的算法题&…

SVN客户端使用

SVN客户端使用1. 复制服务端URL2. 在客户端电脑新建文件夹用于存储版本代码3. 右击空白处&#xff0c;checkout4. 进入trunk目录&#xff0c;即可尝试新建文件并上传到服务端4.其他客户端更新版本5.解决代码冲突期间由于之前登陆过并设置为记住密码&#xff0c;所以整个过程并没…

25 张图,1.4 w字!彻底搞懂分布式事务原理

本文提纲如下&#xff1a;0. 前言1. 单数据源事务 & 多数据源事务2. 常见分布式事务解决方案2.1. 分布式事务模型2.2. 二将军问题和幂等性2.3. 两阶段提交&#xff08;2PC&#xff09; & 三阶段提交&#xff08;3PC&#xff09;方案2.4. TCC 方案2.5. 事务状态表方案2.…

des加密密码补位_密码学中的数据加密标准(DES)

des加密密码补位This is a Data Encryption Standard that is the asymmetric key generation for the encryption of digital data in cryptography. Therefore, its short key length of 56 bits of character criticized from the beginning or starting makes it too insec…

报告老板:这次的缓存事故是这样的...

事故背景公司最近安排了一波商品抢购活动&#xff0c;由于后台小哥操作失误最终导致活动效果差&#xff0c;被用户和代理商投诉了。老板让我带同事们一起复盘这次线上事故。什么原因造成的&#xff1f;抢购活动计划是零点准时开始&#xff0c;22&#xff1a;00 运营人员通过后台…

隐式转换

2019独角兽企业重金招聘Python工程师标准>>> 1&#xff1a;隐式转换应用 1.1 隐式转换为期望类型 隐式转换为期望类型是编译器会使用隐式操作的第一个地方。一旦编译器看到了X&#xff0c;但是需要Y&#xff0c;就会检查从X到Y的隐式转换函数。例如&#xff1a; val…

双“11”搞促销?用贪心算法来盘他!

作者 | 王磊来源 | Java中文社群&#xff08;ID&#xff1a;javacn666&#xff09;转载请联系授权&#xff08;微信ID&#xff1a;GG_Stone&#xff09;这几年商家为了刺激消费是变着花样的推出各种各样的活动&#xff0c;以某多多为首的运营式电商更是让我们看到了营销的无限“…

AndroidStudio使用入门

AndroidStudio使用入门1_AndroidStudio activity的基本使用1.1_MainActivity和activity_main的初识1.2_Activity的清单文件简介1.3_几种重要文件的介绍1.4_基本布局的认识与使用1.4.1_RelativeLayout(相对布局)1.4.2_线性布局2_访问资源的方式2.1_java访问资源的方式2.2_xml访问…

面试官:你说说互斥锁、自旋锁、读写锁、悲观锁、乐观锁的应用场景?

前言生活中用到的锁&#xff0c;用途都比较简单粗暴&#xff0c;上锁基本是为了防止外人进来、电动车被偷等等。但生活中也不是没有 BUG 的&#xff0c;比如加锁的电动车在「广西 - 窃格瓦拉」面前&#xff0c;锁就是形同虚设&#xff0c;只要他愿意&#xff0c;他就可以轻轻松…

2万字,看完这篇才敢说自己真的懂线程池!

前言 线程池可以说是 Java 进阶必备的知识点了&#xff0c;也是面试中必备的考点&#xff0c;可能不少人看了一些文章后能对线程池工作原理说上一二&#xff0c;但这还远远不够&#xff0c;如果碰到比较有经验的面试官再继续追问&#xff0c;很可能会被吊打&#xff0c;考虑如下…

西南大学校园GIS平台

原文:西南大学校园GIS平台系统架构是B/S,开发语言是C#、silverlight&#xff0c;开发平台是.NET&#xff0c;数据库为sqlserver&#xff0c;这是我读研究生时候自己做的作品&#xff0c;以自己的母校为地图&#xff0c;进行GIS相关的功能分析&#xff0c;核心的模块有&#xff…