一个关于多线程协作的题目经常会出现在大厂的面试中:有三个线程分别打印A、B、C,请让这三个线程按顺序打印出ABC20次。
我们知道,线程调度机制是非确定性的,如果不加上额外的并发控制,直接启动三个线程,那么这几个线程的执行顺序是不确定的,打印出来的字符也不一定是按照ABC这样的顺序来显示。同时,题目不光要求按顺序打印出ABC,而且还要打印20次,也就是这样的形式:ABCABCABC...,当然,打印30次或者100次什么的,代码基本上一样,修改一下循环的次数即可。
实现这样的功能有很多方法,我们使用一种简洁方便的方式来完成,同时,为了加深印象和理解,我们尝试采用层层递进的方式来进行讲解,也就是:
启动三个线程,打印出ABC,但无序,可能是BCA,或者CBA等等。
启动三个线程,按照顺序打印出ABC,要确定每次运行都按这个顺序打印。
启用三个线程,按照顺序打印出ABC,不光要按照顺序打印,而且要连续打印20次,也就是ABCABC...这样的形式。
一、打印字符ABC,但无序
这个比较简单,三个线程各打印A、B、C,然后启动这些线程即可,当然,打印出来的字符就是没有顺序的了。代码如下:
package com.sample.interview.multithread;
// 三个线程打印字符ABC,但顺序不确定
public class PrintRandomly {
public static void main(String[] args) {
Thread threadA = new Thread(()-> System.out.print("A"));
Thread threadB = new Thread(()-> System.out.print("B"));
Thread threadC = new Thread(()-> System.out.print("C"));
threadA.start();
threadB.start();
threadC.start();
}
}
二、按顺序打印字符ABC
按顺序打印字符ABC,那我们就得控制这三个线程的执行顺序,线程A先执行,接下来是线程B,最后是线程C。针对类似的并发编程场景,Java提供了一个非常方便实用的类Semaphore,也就是信号量,可以将它看作一个“许可证”,通过它可以实现多线程对于公共资源的并发访问控制,一个线程在获取资源时需要先取得一个许可,否则会阻塞,资源使用完成后,再把许可放回去。
为了按顺序打印ABC,我们控制线程B必须在线程A执行之后再开始执行,线程C类似,所以我们定义三个信号量,初始的许可证数量都是0,因为数量都是0,所以调用信号量的acquire方法时会被阻塞,如果前面的线程执行完成,把许可证数量加1,后面的线程就可以执行了,这样,就实现了顺序打印的功能。
package com.sample.interview.multithread;
import java.util.concurrent.Semaphore;
// 多线程顺序打印出ABC
public class PrintSequentially {
public static void main(String[] args) {
Semaphore semaphoreA = new Semaphore(0);
Semaphore semaphoreB = new Semaphore(0);
Semaphore semaphoreC = new Semaphore(0);
Thread threadA = new Thread(() -> {
try {
semaphoreA.acquire();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.print("A");
semaphoreB.release();
});
Thread threadB = new Thread(() -> {
try {
semaphoreB.acquire();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.print("B");
semaphoreC.release();
});
Thread threadC = new Thread(() -> {
try {
semaphoreC.acquire();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.print("C");
semaphoreA.release();
});
// 线程A的许可证数量加1
semaphoreA.release();
threadC.start();
threadB.start();
threadA.start();
}
}
三、按顺序打印字符ABC20次
有了第二步的铺垫,要实现顺序打印字符ABC20次的功能就相对比较简单了,在每个线程里面直接循环20次即可。因为信号量的acquire方法会尝试去获取一个许可,如果没有就会阻塞,如果拿到了,就把信号量的许可减1,而release方法则是把许可加1,所以,如果前面的线程没有调用这个归还方法,后面的线程是会阻塞的,多次循环也就是简单地把这样的运作多次执行即可。我们把代码稍作提炼如下:
package com.sample.interview.multithread;
import java.util.concurrent.Semaphore;
// 循环打印ABC20次
public class PrintSequentiallyLoop {
public static void main(String[] args) {
Semaphore semaphoreA = new Semaphore(1);
Semaphore semaphoreB = new Semaphore(0);
Semaphore semaphoreC = new Semaphore(0);
PrintChar printCharA = new PrintChar(semaphoreA, semaphoreB,"A");
PrintChar printCharB = new PrintChar(semaphoreB, semaphoreC,"B");
PrintChar printCharC = new PrintChar(semaphoreC, semaphoreA,"C");
new Thread(printCharA).start();
new Thread(printCharB).start();
new Thread(printCharC).start();
}
static class PrintChar implements Runnable{
Semaphore cur;
Semaphore next;
private String str;
private int times = 20;
public PrintChar(Semaphore cur, Semaphore next, String str){
this.cur = cur;
this.next = next;
this.str = str;
}
@Override
public void run() {
for (int i = 0; i < times; i++) {
try {
cur.acquire();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.print(str);
next.release();
}
}
}
}
以上就是多线程顺序打印ABC20次的解决方案,前面也提过,有很多方法可以解决该问题,在当前案例中,我们使用在信号量Semaphore作为并发控制的手段,它的功能强大,使用简单,也便于记忆,后续如果碰到类似的面试题目,我想,只要认真读完本文,是一定能够给出答案的。
都看到这里了,请帮忙一键三连啊,也就是点击文末的在看、点赞、分享,这样会让我的文章让更多人看到,也会大大地激励我进行更多的输出,谢谢!
推荐阅读:
一网打尽:MySQL索引失效的场景大搜罗
这个设计模式的用法,一般人我不告诉他
《论语》是很多公司取名的源泉