【Java多线程系列】经典面试题:使用多线程,循环10次并按顺序打印出ABC

it2025-09-03  4

热门系列:

【Java多线程系列】线程并发与线程忙等待

  程序人生,精彩抢先看


目录

1.前言

问题点分析:

2.上才艺

2.1 方式一:使用AtomicInteger

2.2 方式二:使用线程池newSingleThreadExecutor

2.3 方式三:使用join

2.4 方式四:使用CyclicBarrier

3.总结


1.前言

前段时间,有个同事朋友出去面试,遇到一个有意思的面试题,内容如下:

现有3个线程(命名为T1,T2,T3,后续内容以此描述进行),线程1能打印输出字母A;线程2能打印输出字母B;线程1能打印输出字母C;

要求,循环10次,使3个线程按顺序,每次打印输出 ABC

使最终效果如:ABC  ABC  ABC  ABC  ABC .....

就题目内容而言,乍一看感觉其实很简单,没什么难度。但如果要答出来,其实还得好好思考一番的!

问题点分析:

需要保证T1、T2、T3按先后顺序执行需要保证前一轮3个线程按顺序执行完后,再进入后一轮按顺序执行,否则也会乱序

今天刚好有时间,就自己coding了一波,使用如下4种方式实现出来,供大家一起look look...


2.上才艺

2.1 方式一:使用AtomicInteger

这种方式,其实是我最开始能想到的,最没有技巧可言的。主要思路就是:

使用一个AtomicInteger作为多线程的共享变量(可以理解为多线程的“信号量”),并控制这个共享变量取不同值时,对应的线程才能进行相应的操作。

例如:当AtomicInteger的值为0时,线程T1执行,打印A;值为1时,线程T2执行,打印B;值为2时,线程T3执行,打印C;

如此即可控制3个线程间的执行顺序了!代码如下:

3个线程分别的实现类ClassA、ClassB、ClassC:

package com.giveu.newwebeurekaclient11000.bean; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import java.util.concurrent.atomic.AtomicInteger; @Data @AllArgsConstructor @NoArgsConstructor public class ClassA implements Runnable { private AtomicInteger integer; @Override public void run() { for (;;){ if(integer.intValue() == 0){ System.out.print("A"); integer.incrementAndGet(); break; } } } } package com.giveu.newwebeurekaclient11000.bean; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import java.util.concurrent.atomic.AtomicInteger; @Data @AllArgsConstructor @NoArgsConstructor public class ClassB implements Runnable { private AtomicInteger integer; @Override public void run() { for (;;){ if(integer.intValue() == 1){ System.out.print("B"); integer.incrementAndGet(); break; } } } } package com.giveu.newwebeurekaclient11000.bean; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import java.util.concurrent.atomic.AtomicInteger; @Data @AllArgsConstructor @NoArgsConstructor public class ClassC implements Runnable { private AtomicInteger integer; @Override public void run() { for (;;){ if(integer.intValue() == 2){ System.out.println("C"); integer.set(0); break; } } } }

最后执行类代码如下:

public static void main(String[] args){ final AtomicInteger num = new AtomicInteger(0); try { Thread threadA = new Thread(new ClassA(num)); Thread threadB = new Thread(new ClassB(num)); Thread threadC = new Thread(new ClassC(num)); for (int i = 0; i < 10; i++) { threadA.start(); threadB.start(); threadC.start(); //此处必须使主线程短暂睡眠,否则会乱序 Thread.sleep(50); threadA = new Thread(new ClassA(num)); threadB = new Thread(new ClassB(num)); threadC = new Thread(new ClassC(num)); } } catch (Exception e) { e.printStackTrace(); } }

最终效果如下:

使用此方式,可以保证上面问题分析的第一点,但是不能保障第二点,所以需要加以处理。因此,有两个地方需要注意:

ClassC中run方法中,integer.set(0)必须要有,此为重置“信号量”操作;否则T1、T2将进入死循环main方法中必须加入Thread.sleep(50)使主线程短暂睡眠,否则就不能保证问题分析的第二点

2.2 方式二:使用线程池newSingleThreadExecutor

这个方式个人感觉是最简单的实现方式了。多线程实现类如下:

package com.giveu.newwebeurekaclient11000.bean; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import java.util.concurrent.atomic.AtomicInteger; @Data @AllArgsConstructor @NoArgsConstructor public class ClassA implements Runnable { private int num; @Override public void run() { System.out.print("A"); } }

ClassB、ClassC也类似,调整打印语句即可,就不重复贴出了。

具体main方法实现类如下:

public static void main(String[] args){ ExecutorService singleExecutor = Executors.newSingleThreadExecutor(); try { Thread threadA = new Thread(new ClassA()); Thread threadB = new Thread(new ClassB()); Thread threadC = new Thread(new ClassC()); for (int i = 0; i < 10; i++) { singleExecutor.execute(threadA); singleExecutor.execute(threadB); singleExecutor.execute(threadC); } //关闭线程池 singleExecutor.shutdown(); } catch (Exception e) { e.printStackTrace(); } }

实践结果和上面的结果图一致,我这边就不重复贴出了。各位感兴趣的看官,可自行实践试试。

这种方式最容易实现,但是简单也正是因为使用了单线程的线程池,使得执行操作得以串行化,因此弊端也在此处:

如果是并发量较大的情况下,这种方式效率就不够高了,不能是CPU资源得到充分的利用。 


2.3 方式三:使用join

该方式是使用了多线程之间的线程等待,也是线程通信机制的join方法,来达到线程间的有序执行。线程实现代码同方式二一致,不用改动。main方法稍作调整,代码如下:

public static void main(String[] args){ try { Thread threadA = new Thread(new ClassA()); Thread threadB = new Thread(new ClassB()); Thread threadC = new Thread(new ClassC()); for (int i = 0; i < 10; i++) { threadA.start(); threadA.join(); threadB.start(); threadB.join(); threadC.start(); threadC.join(); threadA = new Thread(new ClassA()); threadB = new Thread(new ClassB()); threadC = new Thread(new ClassC()); } } catch (Exception e) { e.printStackTrace(); } }

此方法也是可以达到解题的效果!该方式的执行流程其实就是:

创建线程之后,主线程开始执行

threadA.start(); threadA.join();

待线程T1执行完后,主线程再依次往下执行T2,待T2执行完后,再到T3,如此循环往复


2.4 方式四:使用CyclicBarrier

这个类CyclicBarrier可以翻译为“循环栅栏”,主要也是通过线程间的通信机制,达到多线程的控制执行。这个类和多线程中另一个类CountDownLatch有些许类似,有兴趣的朋友,可以自行对比了解一番。

此处,咱们接着说使用CyclicBarrier实现解题的效果。首先多线程的实现类代码如下:

package com.giveu.newwebeurekaclient11000.bean; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import java.util.concurrent.BrokenBarrierException; import java.util.concurrent.CyclicBarrier; @Data @AllArgsConstructor @NoArgsConstructor public class ClassA implements Runnable { private CyclicBarrier cyclicBarrier; @Override public void run() { try { while (true){ if(cyclicBarrier.getNumberWaiting() == 0){ System.out.print("A"); cyclicBarrier.await(); break; }else{ Thread.sleep(50); } } } catch (InterruptedException e) { e.printStackTrace(); } catch (BrokenBarrierException e) { e.printStackTrace(); } } } package com.giveu.newwebeurekaclient11000.bean; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import java.util.concurrent.BrokenBarrierException; import java.util.concurrent.CyclicBarrier; @Data @AllArgsConstructor @NoArgsConstructor public class ClassB implements Runnable { private CyclicBarrier cyclicBarrier; @Override public void run() { try { while (true){ if(cyclicBarrier.getNumberWaiting() == 1){ System.out.print("B"); cyclicBarrier.await(); break; }else{ Thread.sleep(50); } } } catch (InterruptedException e) { e.printStackTrace(); } catch (BrokenBarrierException e) { e.printStackTrace(); } } } package com.giveu.newwebeurekaclient11000.bean; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import java.util.concurrent.BrokenBarrierException; import java.util.concurrent.CyclicBarrier; @Data @AllArgsConstructor @NoArgsConstructor public class ClassC implements Runnable { private CyclicBarrier cyclicBarrier; @Override public void run() { try { while (true){ if(cyclicBarrier.getNumberWaiting() == 2){ System.out.println("C"); cyclicBarrier.await(); break; }else{ Thread.sleep(50); } } } catch (InterruptedException e) { e.printStackTrace(); } catch (BrokenBarrierException e) { e.printStackTrace(); } } }

其次,主线程mian方法调用如下:

public static void main(String[] args){ ExecutorService executorService = Executors.newFixedThreadPool(3); CyclicBarrier cyclicBarrier = new CyclicBarrier(3); try { Thread threadA = new Thread(new ClassA(cyclicBarrier)); Thread threadB = new Thread(new ClassB(cyclicBarrier)); Thread threadC = new Thread(new ClassC(cyclicBarrier)); for (int i = 0; i < 10; i++) { executorService.execute(threadA); executorService.execute(threadB); executorService.execute(threadC); } executorService.shutdown(); } catch (Exception e) { e.printStackTrace(); } }

运用此方式,也是可以达到预期效果的。该方式的主要流程是:

通过“循环栅栏”CyclicBarrier,设置3个信号量,当线程循环获取信号量对应数量时,决定了线程T1、T2、T3各自的执行顺序;当每一轮的3个信号量使用完后,则继续下次的循环执行。

在这里,CyclicBarrier主要是决定了上面问题分析中第二点的顺序,但是无法控制第一点所要求的顺序,所以需要通过

cyclicBarrier.getNumberWaiting()

方法来获取到当前本轮次的“信号量数”是为多少,从而决定线程的执行顺序。

但是,在这里我们必须要注意:

方式三和方式四都有一个致命的弊端需要注意,即每个线程间,因为是相互依赖通知而决定执行顺序的,所以,如果有某个线程的执行方法中,出现了错误或异常,没有捕捉处理,导致程序无法正常执行完,则会引起连锁反应,导致整个设计崩溃。


3.总结

虽然我这边给出了多种方式实现了解题的效果,但是每个方式都有自己的优劣之处,所以仅供与大家分享和参考使用。当然,此问题的解题方式远不止上面这4种方式,比如还可以通过同步块synchronized结合wait(),notify()、notifyAll()等,以及通过ReentrantLock与Condition方式等等,都可以实现以上效果。如感兴趣,请各位自行尝试。

最后,如有疑问或有误之处,欢迎各位道友前来探讨指正,Bye.....

 

善良勤劳勇敢而又聪明的老杨 认证博客专家 Java Spring Mysql 一个喜欢学习,热爱分享的Java技术人!年轻人,要坚持学习,耗子尾汁!微信搜索关注时代名猿,免费领取VIP精品学习视频、BAT大厂面试资料、IT技术电子书籍
最新回复(0)