手撕面试题:多个线程交替打印问题
点击上方蓝色“方志朋”,选择“设为星标”
回复“666”获取独家整理的学习资料!
第一篇打算总结下阿里最喜欢问的多个线程顺序打印问题,我遇到的是机试,直接写出运行。同类型的题目有很多,比如
三个线程分别打印 A,B,C,要求这三个线程一起运行,打印 n 次,输出形如“ABCABCABC....”的字符串
两个线程交替打印 0~100 的奇偶数
通过 N 个线程顺序循环打印从 0 至 100
多线程按顺序调用,A->B->C,AA 打印 5 次,BB 打印10 次,CC 打印 15 次,重复 10 次
用两个线程,一个输出字母,一个输出数字,交替输出 1A2B3C4D...26Z
其实这类题目考察的都是线程间的通信问题,基于这类题目,做一个整理,方便日后手撕面试官,文明的打工人,手撕面试题。
使用 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++)
这样。
使用 wait/notify
其实遇到这类型题目,好多同学可能会先想到的就是 join(),或者 wati/notify 这样的思路。算是比较传统且万能的解决方案。也有些面试官会要求不能使用这种方式。
思路:还是以第一题为例,我们用对象监视器来实现,通过 wait
和 notify()
方法来实现等待、通知的逻辑,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,并通知另一线程从等待集合中释放,然后自身线程加入等待集合并释放锁即可。
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();}}
}
使用 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() 这种操作,这样其实是有些浪费资源的。
使用 Semaphore
在信号量上我们定义两种操作: 信号量主要用于两个目的,一个是用于多个共享资源的互斥使用,另一个用于并发线程数的控制。
acquire(获取) 当一个线程调用 acquire 操作时,它要么通过成功获取信号量(信号量减1),要么一直等下去,直到有线程释放信号量,或超时。
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();}}
}
使用 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外卖点餐系统
Redis 分布式锁使用不当,酿成一个重大事故,超卖了100瓶飞天茅台!!!
服务被干爆了!竟然是日志的锅!!
扔掉okhttp、httpClient,来试试这款轻量级HTTP客户端神器?
最近面试BAT,整理一份面试资料《Java面试BAT通关手册》,覆盖了Java核心技术、JVM、Java并发、SSM、微服务、数据库、数据结构等等。获取方式:点“在看”,关注公众号并回复 666 领取,更多内容陆续奉上。
明天见(。・ω・。)ノ♡
手撕面试题:多个线程交替打印问题相关推荐
- 线程打印_经典面试题——两个线程交替打印奇数和偶数
前提 今天下班时候和同事聊天偶然听到面试题"两个线程交替打印奇数和偶数"的实现,这里做一个复盘. 复盘 场景一:线程A打印奇数,线程B打印偶数,线程A和线程B交替打印,使用对象监视 ...
- c语言两个线程交替打印奇数和偶数,经典面试题——两个线程交替打印奇数和偶数...
今天在和同事讨论线程说到了这个我就实现了一把 直接贴代码 public class Demo2 { private static volatile int i = 1; public static v ...
- 线程打印_面试题:用程序实现两个线程交替打印 0~100 的奇偶数
作者:dadiyang来源:https://blog.csdn.net/dadiyang/article/details/88315124 面试场景 面试官:Java多线程了解吗?你给我写一下,起两个 ...
- Java常见的面试算法题:实现两个线程交替打印1到100的数
Java常见的面试算法题:实现两个线程交替打印1到100的数 思路: 这是涉及到多个线程打印的问题,一定会用到锁的(synchronized),故这就是一个多线程打印的典型案例. 代码实现: pack ...
- 三个线程交替打印ABC(Condition实现精确通知)
三个线程交替打印ABC: package pc;import java.util.concurrent.locks.Condition; import java.util.concurrent.loc ...
- java 线程交替输出,[java]java经典问题之线程交替打印数字
问题 给出两个线程,要求两个线程交替打印从1到100,例如:A线程打印1,B线程打印2,A线程打印3...依次类推,直到打印到100 思路 这里主要是考察对java中wait/notifyAll机制的 ...
- 使用Java线程并发库实现两个线程交替打印的线程题
背景:是这样的今天在地铁上浏览了以下网页,看到网上一朋友问了一个多线程的问题.晚上闲着没事就决定把它实现出来. 题目: 1.开启两个线程,一个线程打印A-Z,两一个线程打印1-52的数据. 2.实现交 ...
- 如何让两个线程交替打印整数1-100?你的答案呢?
前端时间下班临走前看到同事做尝试的一个题目:如何让两个线程交替打印整数1-100? 好几年没有写代码玩了,想了想,花了十多分钟写了个答案: #include<stdio.h> #inclu ...
- c语言利用线程交替打印奇偶数,两个线程交替打印奇偶数
序言 以前看过多线程交替打印奇偶数,知道大概怎么写,实际写的时候会卡住,特此记录下来 方法一:wait, notify,性能较差,不推荐使用 public class TestThread { pub ...
最新文章
- linux shell nr,awk中NR和FNR的区别小结和实例演示
- 更好的内存管理-jemalloc (redis 默认使用的)
- ppt流程图字体太小_关于答辩PPT的制作技巧
- python语言入门与精通-Python从入门到精通
- 四部门联合印发《常见类型移动互联网应用程序必要个人信息范围规定》
- python3 集合运算_Python 集合与集合运算
- 支付宝搜索升级:品牌商可在品牌直达专区投放优惠券
- python sqlite3 增删改查(最基本的增删改查)
- 报告:2020年NFT总市值达5200万美元
- Vim 可视化模式入门
- 证件OCR识别360度全面解析
- Java传输文件使用Base64优化传输速率。
- 关于MybatisPlus
- c语言文件操作之图片+文件“合成器”(详解+源码+视频讲解)
- ThinkPad电脑黑屏只显示鼠标
- Vue项目关闭格式检查命令
- Tiktok 网络、网络
- 第八部份:Bless安装
- 【区块链开发】区块链农产品溯源App
- 卡夫卡详解_卡夫卡概念
热门文章
- pycharm远程调试或运行代码
- MySQL数据copy
- XML(eXtensible Markup Language)文件的解析
- 技术图文:举例详解Python中 split() 函数的使用方法
- 【算法导论】冒泡排序 选择排序
- iOS15.4 来袭:新增“男妈妈”表情及口罩面容解锁、AirTags 反跟踪等新功能
- 百度副总裁马杰:实现元宇宙,技术要过三道坎
- 隐私数据在隐私AI框架中的安全流动
- 打通语言理论和统计NLP,Transformers/GNNs架构能做到吗?
- 抖音、快手和直播行业的火爆究竟给了谁机会?