问题
三个线程轮流打印,其中一个线程打印a,一个打印b,最后一个打印c。最终形成abcabcabc…这样的字符串
考虑点:
- 字符串的第一个字符必须是a,很多方案在代码层级按ABC启动了线程,尽管大概率会遵循代码的先后顺序,但实际上谁先启动成功或谁先获得锁,逻辑上仍然是无法确定的,这也是目前很多网传方案的一大问题,我们在考虑多线程竞争时,必须达成逻辑严谨。而不是依靠代码顺序,或sleep来推断
- 此处是三个线程,如果是26个线程,分别打印a b c d … z 呢?
- 能不能用多种方案实现
思考
这种题目主要考查的就是多线程之间的通信问题,当然,这题也可以用一些“歪点子”,比如以时间为轴,各个线程只在特定的时间运行那么一下,达到轮流的效果。只是这样的方式恐怕不会令面试官满意。所以最好还是回到线程通信的范围里来。
一般来讲,广义上线程的通信无非两种,
- 一种是直接的线程操作,比如 a 线程直接去操作 b 线程的运行、中断、停止等;
另一种则是共享内存,两个线程都可以访问某内存位置,根据该位置的值或者状态,每个线程再来决定自己该做什么,即通过共享来达成“消息传递”,当然,这里的内存位置就必须支持并发安全了,你可以使用volatile 或者是 同步结构 来保证并发安全
如果还有其他方式,则一般是上述两者的结合或变种
回答
方法一:线程 suspend 与 resume
我们一开始选用最符合直觉的,线程间直接执行操作,但是我们知道线程如果被停止,就进入终态,无法恢复了,而使用中断标记,只能为别的线程设置中断,却无法为别的线程取消中断,因此一个相对可行的方法是使用suspend 和 resume,达到令其停止执行代码的目的。
方法要点:
- 通信方法 —— 因为采用的线程直接操作,至少需要对线程有停止 和 启动 两种相反的操作同时还不能令线程进入终态,因此选用的 suspend(挂起) 和 resume(恢复)【两种方法均被废弃,因此不太推荐】
- 循环问题 —— 因为直接引用的方式需要每个线程都持有下个线程的引用,但代码写在前面的线程无法获取到后面线程的引用。所以此处用 List 把线程都存起来,这样所有线程都能取到引用了。
- 输出顺序 —— 主线程启动A线程,A线程启动B线程。需要特别注意的是,尽管有这种启动顺序,但不代表会有同样的输出顺序,比如A启动B,但B可能比A先输出了。因此每个线程必须先完成第一次输出,再启动下个线程,当所有线程都运行起来后,再由通信方法保证输出顺序
public class ThreeThread { public static void main(String[] args) { List<Thread> list = new ArrayList<>(); Thread threadA = new Thread(new Runnable() { @Override public void run() { while (true) { System.out.print("a"); list.get(1).resume(); if (!Objects.equals(list.get(1).getState(), Thread.State.RUNNABLE)) { list.get(1).start(); } Thread.currentThread().suspend(); } } }); list.add(threadA); Thread threadB = new Thread(new Runnable() { @Override public void run() { while (true) { System.out.print("b"); list.get(2).resume(); if (!Objects.equals(list.get(2).getState(), Thread.State.RUNNABLE)) { list.get(2).start(); } Thread.currentThread().suspend(); } } }); list.add(threadB); Thread threadC = new Thread(new Runnable() { @Override public void run() { while (true) { System.out.print("c"); list.get(0).resume(); Thread.currentThread().suspend(); } } }); list.add(threadC); threadA.start(); } }
方法二:volatile 变量
如果选用一个变量,来做线程通信的媒介,选用volatile变量肯定是个很好的办法,因为它的改变对所有线程可见。当然,我们需要注意,这种变量毕竟不是锁,因此不能对其并发修改,需要由代码层面控制,一次只有一个线程对其修改
方法要点:
- 通信方法 —— 采用volatile变量,利用了volatile变量对所有线程实时可见的特性
- 输出顺序 —— 线程的启动顺序虽然是ABC,但哪个线程先执行其实未可知,但是没有关系,我们使用了变量的值来做判断,即使B线程先执行了,因为flag初始值为0,所以线程B也只能空循环,直到A线程将flag改为1,线程B才能进行打印
public class ThreeThread2 { static volatile int flag = 0; public static void main(String[] args) { Thread threadA = new Thread(new Runnable() { @Override public void run() { while (true) { if (flag == 0) { System.out.print("a"); flag = 1; } } } }); Thread threadB = new Thread(new Runnable() { @Override public void run() { while (true) { if (flag == 1) { System.out.print("b"); flag = 2; } } } }); Thread threadC = new Thread(new Runnable() { @Override public void run() { while (true) { if (flag == 2) { System.out.print("c"); flag = 0; } } } }); threadA.start(); threadB.start(); threadC.start(); } }
方法三:synchronized 锁 (一)
除了利用一个基本类型变量来通信,我们也可以利用对象来通信,最好的对象当然是支持同步的锁了,所以我们不妨使用jdk自带的关键字synchronized 来实现,每个线程都唤醒下个线程,然后自己沉睡。但我们首先得明白synchronized 的不足:synchronized只有一个等待队列,且每次唤醒只能唤醒队列的头节点线程。这意味着,当线程B第一次进入同步代码块,想唤醒下个线程C时,第三个线程C此时却不在等待队列中,它的唤醒只会把刚刚进入队列的线程A唤醒,这样就成了A -> B -> A了,不符合要求的ABC顺序。因此,必须对线程B的第一次同步代码块特殊处理
方法要点:
- 通信方法 —— 利用synchronized 同步代码块里的 notify(唤醒) 和 wait(等待)
循环问题 —— 因为多个线程使用synchronized时,无法确定谁会获得锁,所以采用线程嵌套启动的方式来按顺序启动所有线程,并使其按该顺序第一次获得锁,所以启动下一线程的代码需写在同步代码块中!另外,代码写在前面的线程无法获取到后面线程的引用,所以此处用 List 把线程都存起来,这样所有线程都能取到引用了。
- 输出顺序 —— 虽然一开始所有线程能够按顺序获取锁,但是获取锁后却无法唤醒下一个线程,因为此时下一个线程不在等待队列里。所以中间的线程第一次不能执行唤醒操作,只有当所有线程都按顺序在等待队列中排号后,利用了等待队列的FIFO特性,才能正常唤醒下个线程
public class ThreeThread3 { public static void main(String[] args) { Object o = new Object(); List<Thread> list = new ArrayList<>(); Thread threadA = new Thread(new Runnable() { @Override public void run() { while (true) { synchronized (o) { try { System.out.print("a"); o.notify(); if (Objects.equals(list.get(1).getState(), Thread.State.NEW)) { list.get(1).start(); } o.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } } }); list.add(threadA); Thread threadB = new Thread(new Runnable() { @Override public void run() { // 中间的线程都需要特殊处理,我们的目的是使所有线程一开始都按启动顺序进入等待队列中,然后才开始循环 synchronized (o) { try { System.out.print("b"); if (Objects.equals(list.get(2).getState(), Thread.State.NEW)) { list.get(2).start(); } o.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } while (true) { synchronized (o) { try { System.out.print("b"); o.notify(); o.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } } }); list.add(threadB); Thread threadC = new Thread(new Runnable() { @Override public void run() { while (true) { synchronized (o) { try { System.out.println("c"); o.notify(); o.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } } }); list.add(threadC); threadA.start(); } }
方法四:synchronized 锁 (二)
方法三我们利用了synchronized 的等待队列的FIFO的顺序来保证循环输出,但我们也可以不利用其内部队列的FIFO来实现顺序,而是利用多个锁的嵌套。三个线程就建三个锁,例如线程A只有同时获得锁A和锁B后,才能正常输出,而后把B锁让给B线程,自己进入A锁的等待队列
public class ThreeThread4 { public static void main(String[] args) { List<Thread> list = new ArrayList<>(); Thread threadA = new Thread(new Runnable() { @Override public void run() { while (true) { // 同时获得A B两把锁才打印 synchronized (list.get(0)){ synchronized (list.get(1)){ System.out.print("a"); if (Objects.equals(list.get(1).getState(), Thread.State.NEW)) { list.get(1).start(); } // 准备放弃B锁,需要先唤醒其他线程(此处是线程B)来接盘 list.get(1).notifyAll(); } try { // 进入A锁的等待队列 list.get(0).wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } } }); list.add(threadA); Thread threadB = new Thread(new Runnable() { @Override public void run() { while (true) { synchronized (list.get(1)){ synchronized (list.get(2)){ System.out.print("b"); if (Objects.equals(list.get(2).getState(), Thread.State.NEW)) { list.get(2).start(); } list.get(2).notifyAll(); } try { list.get(1).wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } } }); list.add(threadB); Thread threadC = new Thread(new Runnable() { @Override public void run() { while (true) { synchronized (list.get(2)){ synchronized (list.get(0)){ System.out.println("c"); list.get(0).notifyAll(); } try { list.get(2).wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } } }); list.add(threadC); threadA.start(); } }
方法五:Lock 与 Condition
除了synchronized 锁,我们还可以用Lock 与 Condition 来实现,虽然同synchronized 一样,我们没法指定唤醒哪个线程,但是我们可以选择唤醒哪条条件队列,只要保证一个条件队列只要有一个线程即可。因此,使用Lock 与 Condition 可以获得比方法三和四更简单的代码
public class ThreeThread5 { public static void main(String[] args) { List<Thread> list = new ArrayList<>(); Lock lock = new ReentrantLock(); Condition conditionA = lock.newCondition(); Condition conditionB = lock.newCondition(); Condition conditionC = lock.newCondition(); Thread threadA = new Thread(new Runnable() { @Override public void run() { while (true) { try { lock.lock(); System.out.print("a"); if (Objects.equals(list.get(1).getState(), Thread.State.NEW)) { list.get(1).start(); } conditionB.signal(); conditionA.await(); } catch (InterruptedException e) { e.printStackTrace(); } } } }); list.add(threadA); Thread threadB = new Thread(new Runnable() { @Override public void run() { while (true) { try { lock.lock(); System.out.print("b"); if (Objects.equals(list.get(2).getState(), Thread.State.NEW)) { list.get(2).start(); } conditionC.signal(); conditionB.await(); } catch (InterruptedException e) { e.printStackTrace(); } } } }); list.add(threadB); Thread threadC = new Thread(new Runnable() { @Override public void run() { while (true) { try { lock.lock(); System.out.println("c"); conditionA.signal(); conditionC.await(); } catch (InterruptedException e) { e.printStackTrace(); } } } }); list.add(threadC); threadA.start(); } }
优化
我们提供的五个方法仅仅是一部分,需要强调的是,不是由主线程依次启动线程ABC,就代表线程A就一定比线程B先执行,所以网传类似的方法大多有不严谨的地方。
另外,我们现在仅仅是三个线程,所以代码里采用的都是朴素的new Thread方法,如果有几十个线程呢,不可能每个线程都需要我们手写其中的run方法吧,所以我们可以使用继承Thread类的方式来完成更多线程的顺序输出,以下,我们以方案二为例子写代码
public class ThreeThread6 { static volatile Integer flag = 0; public static void main(String[] args) { MyThread threadA = new MyThread(0 , 1, "a"); MyThread threadB = new MyThread(1 , 2, "b"); MyThread threadC = new MyThread(2 , 0, "c"); threadA.start(); threadB.start(); threadC.start(); } // 写一个内部类来减少重复代码 private static class MyThread extends Thread { Integer myFlag; Integer nextFlag; String myStr; public MyThread(Integer myFlag, Integer nextFlag, String myStr) { this.myFlag = myFlag; this.nextFlag = nextFlag; this.myStr = myStr; } @Override public void run() { while (true) { if (flag.equals(myFlag) ) { System.out.print(myStr); flag = nextFlag; } } } } }