本文博主给大家讲解一道网上非常经典的多线程面试题目。关于三个线程如何交替打印 ABC 循环 100 次的问题。
下文实现代码都基于 Java 代码在单个 JVM 内实现。
问题描述
给定三个线程,分别命名为 A、B、C,要求这三个线程按照顺序交替打印 ABC,每个字母打印 100 次,最终输出结果为:
推荐博主开源的 H5 商城项目 waynboot-mall,这是一套全部开源的微商城项目,包含三个项目:运营后台、H5 商城前台和服务端接口。实现了商城所需的首页展示、商品分类、商品详情、商品 sku、分词搜索、购物车、结算下单、支付宝/微信支付、收单评论以及完善的后台管理等一系列功能。 技术上基于最新得 Springboot3.0、jdk17,整合了 MySql、Redis、RabbitMQ、ElasticSearch 等常用中间件。分模块设计、简洁易维护,欢迎大家点个 star、关注博主。
github 地址:https://github.com/wayn111/waynboot-mall
解决思路
这是一个典型的多线程同步的问题,需要保证每个线程在打印字母之前,能够判断是否轮到自己执行,以及在打印字母之后,能够通知下一个线程执行。为了实现这一目标,博主讲介绍以下 5 种方法:
方法一:使用 synchronized 和 wait/notify
synchronized 是 Java 中的一个关键字,用于实现对共享资源的互斥访问。wait 和 notify 是 Object 类中的两个方法,用于实现线程间的通信。wait 方法会让当前线程释放锁,并进入等待状态,直到被其他线程唤醒。notify 方法会唤醒一个在同一个锁上等待的线程。
我们可以使用一个共享变量 state 来表示当前应该打印哪个字母,初始值为 0。当 state 为 0 时,表示轮到 A 线程打印;当 state 为 1 时,表示轮到 B 线程打印;当 state 为 2 时,表示轮到 C 线程打印。每个线程在打印完字母后,需要将 state 加 1,并对 3 取模,以便循环。同时,每个线程还需要唤醒下一个线程,并让自己进入等待状态。
具体的代码实现如下:
public class PrintABC {
// 共享变量,表示当前应该打印哪个字母
private static int state = 0;
// 共享对象,作为锁和通信的媒介
private static final Object lock = new Object();
public static void main(String[] args) {
// 创建三个线程
Thread threadA = new Thread(new Runnable() {
@Override
public void run() {
try {
// 循环100次
for (int i = 0; i < 100; i++) {
// 获取锁
synchronized (lock) {
// 判断是否轮到自己执行
while (state % 3 != 0) {
// 不是则等待
lock.wait();
}
// 打印字母
System.out.println("A");
// 修改状态
state++;
// 唤醒下一个线程
lock.notifyAll();
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread threadB = new Thread(new Runnable() {
@Override
public void run() {
try {
for (int i = 0; i < 100; i++) {
synchronized (lock) {
while (state % 3 != 1) {
lock.wait();
}
System.out.println("B");
state++;
lock.notifyAll();
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread threadC = new Thread(new Runnable() {
@Override
public void run() {
try {
for (int i = 0; i < 100; i++) {
synchronized (lock) {
while (state % 3 != 2) {
lock.wait();
}
System.out.println("C");
state++;
lock.notifyAll();
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
// 启动三个线程
threadA.start();
threadB.start();
threadC.start();
}
}
复制代码
方法二:使用 ReentrantLock 和 Condition
ReentrantLock 是 Java 中的一个类,用于实现可重入的互斥锁。Condition 是 ReentrantLock 中的一个接口,用于实现线程间的条件等待和唤醒。ReentrantLock 可以创建多个 Condition 对象,每个 Condition 对象可以绑定一个或多个线程,实现对不同线程的精确控制。
我们可以使用一个 ReentrantLock 对象作为锁,同时创建三个 Condition 对象,分别绑定 A、B、C 三个线程。每个线程在打印字母之前,需要调用对应的 Condition 对象的 await 方法,等待被唤醒。每个线程在打印字母之后,需要调用下一个 Condition 对象的 signal 方法,唤醒下一个线程。
具体的代码实现如下:
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class PrintABC {
// 共享变量,表示当前应该打印哪个字母
private static int state = 0;
// 可重入锁
private static final ReentrantLock lock = new ReentrantLock();
// 三个条件对象,分别绑定A、B、C三个线程
private static final Condition A = lock.newCondition();
private static final Condition B = lock.newCondition();
private static final Condition C = lock.newCondition();
public static void main(String[] args) {
// 创建三个线程
Thread threaA = new Thread(new Runnable() {
@Override
public void run() {
try {
// 循环100次
for (int i = 0; i < 100; i++) {
// 获取锁
lock.lock();
try {
// 判断是否轮到自己执行
while (state % 3 != 0) {
// 不是则等待
A.await();
}
// 打印字母
System.out.println("A");
// 修改状态
state++;
// 唤醒下一个线程
B.signal();
} finally {
// 释放锁
lock.unlock();
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread threaB = new Thread(new Runnable() {
@Override
public void run() {
try {
for (int i = 0; i < 100; i++) {
lock.lock();
try {
while (state % 3 != 1) {
B.await();
}
System.out.println("B");
state++;
C.signal();
} finally {
lock.unlock();
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread threaC = new Thread(new Runnable() {
@Override
public void run() {
try {
for (int i = 0; i < 100; i++) {
lock.lock();
try {
while (state % 3 != 2) {
C.await();
}
System.out.println("C");
state++;
A.signal();
} finally {
lock.unlock();
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
// 启动三个线程
threaA.start();
threaB.start();
threaC.start();
}
}
复制代码
方法三:使用 Semaphore
Semaphore 是 Java 中的一个类,用于实现信号量机制。信号量是一种计数器,用于控制对共享资源的访问。Semaphore 可以创建多个信号量对象,每个信号量对象可以绑定一个或多个线程,实现对不同线程的精确控制。
我们可以使用三个 Semaphore 对象,分别初始化为 1、0、0,表示 A、B、C 三个线程的初始许可数。每个线程在打印字母之前,需要调用对应的 Semaphore 对象的 acquire 方法,获取许可。每个线程在打印字母之后,需要调用下一个 Semaphore 对象的 release 方法,释放许可。
具体的代码实现如下:
import java.util.concurrent.Semaphore;
public class PrintABC {
private static int state = 0;
// 三个信号量对象,分别表示A、B、C三个线程的初始许可数
private static final Semaphore A = new Semaphore(1);
private static final Semaphore B = new Semaphore(0);
private static final Semaphore C = new Semaphore(0);
public static void main(String[] args) {
// 创建三个线程
Thread threadA = new Thread(new Runnable() {
@Override
public void run() {
try {
// 循环100次
for (int i = 0; i < 100; i++) {
// 获取许可
A.acquire();
// 打印字母
System.out.println("A");
// 修改状态
state++;
// 释放许可
B.release();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread threadB = new Thread(new Runnable() {
@Override
public void run() {
try {
for (int i = 0; i < 100; i++) {
B.acquire();
System.out.println("B");
state++;
C.release();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread threadC = new Thread(new Runnable() {
@Override
public void run() {
try {
for (int i = 0; i < 100; i++) {
C.acquire();
System.out.println("C");
state++;
A.release();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
// 启动三个线程
threadA.start();
threadB.start();
threadC.start();
}
}
复制代码
方法四:使用 AtomicInteger 和 CAS
AtomicInteger 是 Java 中的一个类,用于实现原子性的整数操作。CAS 是一种无锁的算法,全称为 Compare And Swap,即比较并交换。CAS 操作需要三个参数:一个内存地址,一个期望值,一个新值。如果内存地址的值与期望值相等,就将其更新为新值,否则不做任何操作。
我们可以使用一个 AtomicInteger 对象来表示当前应该打印哪个字母,初始值为 0。当 state 为 0 时,表示轮到 A 线程打印;当 state 为 1 时,表示轮到 B 线程打印;当 state 为 2 时,表示轮到 C 线程打印。每个线程在打印完字母后,需要使用 CAS 操作将 state 加 1,并对 3 取模,以便循环。
具体的代码实现如下:
import java.util.concurrent.atomic.AtomicInteger;
public class PrintABC {
// 共享变量,表示当前应该打印哪个字母
private static AtomicInteger state = new AtomicInteger(0);
public static void main(String[] args) {
// 创建三个线程
Thread threadA = new Thread(new Runnable() {
@Override
public void run() {
// 循环100次
for (int i = 0; i < 100; ) {
// 判断是否轮到自己执行
if (state.get() % 3 == 0) {
// 打印字母
System.out.println("A");
// 修改状态,使用CAS操作保证原子性
state.compareAndSet(state.get(), state.get() + 1);
// 计数器加1
i++;
}
}
}
});
Thread threadB = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 100; ) {
if (state.get() % 3 == 1) {
System.out.println("B");
state.compareAndSet(state.get(), state.get() + 1);
i++;
}
}
}
});
Thread threadC = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 100; ) {
if (state.get() % 3 == 2) {
System.out.println("C");
state.compareAndSet(state.get(), state.get() + 1);
i++;
}
}
}
});
// 启动三个线程
threadA.start();
threadB.start();
threadC.start();
}
}
复制代码
方法五:使用 CyclicBarrier
CyclicBarrier 是 Java 中的一个类,用于实现多个线程之间的屏障。CyclicBarrier 可以创建一个屏障对象,指定一个参与等待线程数和一个到达屏障点时得动作。当所有线程都到达屏障点时,会执行屏障动作,然后继续执行各自的任务。CyclicBarrier 可以重复使用,即当所有线程都通过一次屏障后,可以再次等待所有线程到达下一次屏障。
我们可以使用一个 CyclicBarrier 对象,指定三个线程为参与等待数,以及一个打印字母的到达屏障点动作。每个线程在执行完自己的任务后,需要调用 CyclicBarrier 对象的 await 方法,等待其他线程到达屏障点。当所有线程都到达屏障点时,会执行打印字母的屏障动作,并根据 state 的值判断应该打印哪个字母。然后,每个线程继续执行自己的任务,直到循环结束。需要注意得就是由于打印操作在到达屏障点得动作内执行,所以三个线程得循环次数得乘以参与线程数量,也就是三。
具体的代码实现如下:
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
public class PrintABC {
// 共享变量,表示当前应该打印哪个字母
private static int state = 0;
// 参与线程数量
private static int threadNum = 3;
// 循环屏障,指定三个线程为屏障点,以及一个打印字母的屏障动作
private static final CyclicBarrier barrier = new CyclicBarrier(threadNum, new Runnable() {
@Override
public void run() {
// 根据state的值判断应该打印哪个字母
switch (state) {
case 0:
System.out.println("A");
break;
case 1:
System.out.println("B");
break;
case 2:
System.out.println("C");
break;
}
// 修改状态
state = (state + 1) % 3;
System.out.println(state);
}
});
public static void main(String[] args) {
// 创建三个线程
Thread threadA = new Thread(new Runnable() {
@Override
public void run() {
try {
// 循环100次
for (int i = 0; i < threadNum * 100; i++) {
// 执行自己的任务
// ...
// 等待其他线程到达屏障点
barrier.await();
}
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
}
});
Thread threadB = new Thread(new Runnable() {
@Override
public void run() {
try {
for (int i = 0; i < threadNum * 100; i++) {
// 执行自己的任务
// ...
// 等待其他线程到达屏障点
barrier.await();
}
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
}
});
Thread threadC = new Thread(new Runnable() {
@Override
public void run() {
try {
for (int i = 0; i < threadNum * 100; i++) {
// 执行自己的任务
// ...
// 等待其他线程到达屏障点
barrier.await();
}
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
}
});
// 启动三个线程
threadA.start();
threadB.start();
threadC.start();
}
}
复制代码
总结
到此,本文内容已经讲解完毕,以上的这五种方法都可以利用不同的工具和机制来实现多线程之间的同步和通信,从而保证按照顺序交替打印 ABC。这些方法各有优缺点,具体的选择需要根据实际的场景和需求来决定。
最后本文讲解代码是在单个 JVM 内的实现方法,如果大家对涉及到多个 JVM 来实现按照顺序交替打印 ABC 的话,可以私信博主,博主再给大家出一期文章进行讲解。
关注公众号【waynblog】每周分享技术干货、开源项目、实战经验、高效开发工具等,您的关注将是我的更新动力!
评论