让3个线程依次打印ABC,是一个常见的(面试)问题,这是阿里和华为都有考过的面试题。比如题目描述如下:
编写一个程序,开启三个线程,这三个线程按照顺序依次打印ABC,每个字母打印10次后结束,最后结果如 ABCABCABC... 依次递推
这是一道经典的多线程编程面试题,首先吐槽一下,这道题的需求很是奇葩,先开启多线程,然后再串行打印 ABC,这不是吃饱了撑的吗?不过既然是道面试题,就不管这些了,其目的在于考察你的多线程编程基础。
这个题目肯定是要启动3个线程的,那怎么让这3个线程“协作”按顺序打印A、B、C呢?从大的方面来讲,这种**“协作”**可分为以下两种:
- 竞争型:每个线程都抢着去打印,如果发现不该自己打印,则准备下一轮抢。由于大家都是竞争的,因此需要用锁机制来保护。
- 协同型:当前线程线程打印之后通知下一个线程去打印,这种需要确认好第一个线程打印时机。由于是协同型的因此可以不用锁机制来保护,但是需要一个通知机制。
竞争型打印
多个线程竞争型打印,优势是代码简单易懂,劣势是线程争抢是CPU调度进行的,可能该某个线程打印时结果该线程迟迟未被CPU调度,结果其他线程被CPU调度到但是由于不能执行打印操作而继续争抢,造成CPU性能浪费。示例代码如下:
@AllArgsConstructor
public class DemoTask implements Runnable {
// 这里将lock对象换成 Lock(ReentrantLock) 进行lock/unlock也是可以的
private static final Object lock = new Object();
private static final int MAX = 30;
private static int current = 0;
private int index;
@Override
public void run() {
while (current < MAX) {
synchronized (lock) {
if ((current < MAX) && (current % 3 == index)) {
System.out.println((char) ('A' + current % 3));
current++;
}
}
}
}
public static void main(String[] args) throws Exception {
List<Thread> threadList = Arrays.asList(
new Thread(new DemoTask(0)),
new Thread(new DemoTask(1)),
new Thread(new DemoTask(2))
);
threadList.forEach(Thread::start);
}
}
协同型打印
多个线程协同型打印,优势是各个线程使用“通知”机制进行协同分工,理论上执行效率较高,不过要使用对应的“通知”机制。关于如何“通知”,第一种是可使用Java对象的 wait/notify
或者Conditon
对象的await/signal
,第二种是以事件或者提交任务的方式(比如通过提交“待打印数字”这个任务给下一个线程)。
第一种方式网上对应的示例代码很多,就不在赘述。下面以第二种方式进行代码分析,打印完成之后,将待打印的数据塞给下一个线程,这样下一个线程就可以打印了,代码如下:
public static void main(String[] args) {
ThreadTask t1 = new ThreadTask();
ThreadTask t2 = new ThreadTask();
ThreadTask t3 = new ThreadTask();
t1.next = t2;
t2.next = t3;
t3.next = t1;
t1.start();
t2.start();
t3.start();
t1.queue.add(0);
}
public static class ThreadTask extends Thread {
private static final Integer END = 30;
@Getter
private BlockingQueue<Integer> queue = new LinkedBlockingQueue<>();
private ThreadTask next;
@SneakyThrows
@Override
public void run() {
while (true) {
Integer take = queue.take();
if (take >= END) {
break;
}
System.out.println((char) ('A' + take % 3));
next.queue.put(take + 1);
}
if (next.isAlive()) {
next.queue.put(END);
}
}
}
代码中使用BlockingQueue
队列,避免了 wait/notify
或者`` await/signal `,也能达到通知机制。注意,Java的阻塞队列是一个支持阻塞插入和移除方法的队列,阻塞队列常用于生产者和消费者场景,生产者是向队列中添加元素的线程,消费者是从队列中获取数据的线程。
阻塞队列主要包括两部分内容:一个是存放数据的容器,另一个就是线程的管理(阻塞/唤醒),前者可以基于Array或者LinkedList数据结构,后者借助于Lock/Condition来实现,也就是使用通知模式来实现的,具体可查看Lock/Condition资料。
推荐阅读