三个线程T1、T2、T3,如何让他们按顺序执行?
这是一道面试中常考的并发编程的代码题,与它相似的问题有:
- 三个线程T1、T2、T3轮流打印ABC,打印n次,如ABCABCABCABC…….
- 两个线程交替打印1-100的奇偶数
- N个线程循环打印1-100
- ……
其实这类问题本质上都是线程通信问题,思路基本上都是一个线程执行完毕,阻塞该线程,唤醒其他线程,按顺序执行下一个线程。下面先来看最简单的,如何按顺序执行三个线程。
synchronized+wait/notify
基本思路就是线程A、线程B、线程C三个线程同时启动,因为变量num
的初始值为0
,所以线程B或线程C拿到锁后,进入while()
循环,然后执行wait()
方法,线程线程阻塞,释放锁。只有线程A拿到锁后,不进入while()
循环,执行num++
,打印字符A
,最后唤醒线程B和线程C。此时num
值为1
,只有线程B拿到锁后,不被阻塞,执行num++
,打印字符B
,最后唤醒线程A和线程C,后面以此类推。
1 | java复制代码class Wait_Notify_ACB { |
输入结果:
1 | java复制代码ABC |
接下来看看第一个问题,三个线程T1、T2、T3轮流打印ABC,打印n次。其实只需要将上述代码加一个循环即可,这里假设n=10。
1 | java复制代码class Wait_Notify_ACB { |
输出结果:
1 | java复制代码ABCABCABCABCABCABCABCABCABCABC |
下面看第二个问题,两个线程交替打印1-100的奇偶数,为了减少输入所占篇幅,这里将100 改成了10。基本思路上面类似,线程odd先拿到锁——打印数字——唤醒线程even——阻塞线程odd,以此循环。
1 | java复制代码class Wait_Notify_Odd_Even{ |
运行结果:
1 | java复制代码odd:1 |
再看第三个问题,N个线程循环打印1-100,其实仔细想想这个和三个线程循环打印ABC并没有什么本质区别,只需要加上判断是否到了打印数字的最大值的语句即可。假设N=3,为了能把输出结果完全显示,打印1-10,代码如下:
1 | java复制代码class Wait_Notify_100 { |
输出结果:
1 | java复制代码thread1: 1 |
面试官: 大家都是用的synchronized+wait/notify
,你能不能换个方法解决该问题?
我: 好的,我还会用join()
方法
下面介绍的方法只给出第一道题的代码了,否则太长了,相信大家可以举一反三
join()
join()
方法:在A线程中调用了B线程的join()方法时,表示只有当B线程执行完毕时,A线程才能继续执行。基于这个原理,我们使得三个线程按顺序执行,然后循环多次即可。无论线程1、线程2、线程3哪个先执行,最后执行的顺序都是线程1——>线程2——>线程3。代码如下:
1 | java复制代码class Join_ABC { |
输出结果:
1 | java复制代码ABCABCABCABCABCABCABCABCABCABC |
面试官: 还会其他方法吗?
我: 还会使用Lock解决该问题。
Lock
该方法很容易理解,不管哪个线程拿到锁,只有符合条件的才能打印。代码如下:
1 | java复制代码 class Lock_ABC { |
输出结果:
1 | java复制代码ABCABCABCABCABCABCABCABCABCABC |
面试官: 该方法存在什么问题,可以进一步优化吗
我: 可以使用Lock+Condition实现对线程的精准唤醒,减少对同步锁的无意义竞争,浪费资源。
Lock+Condition
该思路和synchronized+wait/notify方法的很像,synchronized对应lock,await/signal方法对应wait/notify方法。下面的代码为了能精准地唤醒下一个线程,创建了多个Condition对象。
1 | java复制代码class LockConditionABC { |
输出结果:
1 | java复制代码ABCABCABCABCABCABCABCABCABCABC |
面试官: 除了该方法,还有什么方法可以避免唤醒其他无意义的线程避免资源浪费?
我: 可以通过使用信号量来实现。
Semaphore
Semaphore:用来控制同时访问某个特定资源的操作数量,或者同时执行某个制定操作的数量。Semaphore内部维护了一个计数器,其值为可以访问的共享资源的个数。
一个线程要访问共享资源,先使用acquire()
方法获得信号量,如果信号量的计数器值大于等于1,意味着有共享资源可以访问,则使其计数器值减去1,再访问共享资源。如果计数器值为0,线程进入休眠。
当某个线程使用完共享资源后,使用release()
释放信号量,并将信号量内部的计数器加1,之前进入休眠的线程将被唤醒并再次试图获得信号量。
代码如下:
1 | java复制代码class SemaphoreABC { |
输出结果:
1 | java复制代码ABCABCABCABCABCABCABCABCABCABC |
面试官: 除了上述五种方法,还有其他方法吗
我: 还有LockSupport、CountDownLatch、AtomicInteger等等。
面试官: 那如何实现三个线程循环打印ACB,其中A打印两次,B打印三次,C打印四次呢?
我: ……
面试官: 如何用两个线程交叉打印数字和字符呢?例如A1B2C3……Z26
我: ……
真正的面试过程中,肯定不会让大家用这么多方法实现多线程交替打印问题,记住一两种即可,大家可以思考下后面两个升级版的问题,原理都是相通的。
微信搜索公众号路人zhang,回复面试手册,领取更多高频面试题PDF版及更多面试资料。
推荐阅读:
本文转载自: 掘金