本文博主给咱们解说一道网上非常经典的多线程面试题目。关于三个线程怎么替换打印ABC循环100次的问题。
下文完成代码都基于Java代码在单个JVM内完成。
问题描述
给定三个线程,别离命名为A、B、C,要求这三个线程依照顺序替换打印ABC,每个字母打印100次,最终输出结果为:
A
B
C
A
B
C
...
A
B
C
引荐博主开源的 H5 商城项目waynboot-mall,这是一套悉数开源的微商城项目,包括三个项目:运营后台、H5 商城前台和服务端接口。完成了商城所需的主页展现、产品分类、产品概况、产品 sku、分词搜索、购物车、结算下单、支付宝/微信支付、收单评论以及完善的后台管理等一系列功用。 技术上基于最新得 Springboot3.0、jdk17,整合了 MySql、Redis、RabbitMQ、ElasticSearch 等常用中间件。分模块设计、简洁易保护,欢迎咱们点个 star、重视博主。
github 地址:github.com/wayn111/way…
处理思路
这是一个典型的多线程同步的问题,需求确保每个线程在打印字母之前,能够判别是否轮到自己履行,以及在打印字母之后,能够通知下一个线程履行。为了完成这一方针,博主讲介绍以下5种办法:
- 运用synchronized和wait/notify
- 运用ReentrantLock和Condition
- 运用Semaphore
- 运用AtomicInteger和CAS
- 运用CyclicBarrier
办法一:运用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】每周分享技术干货、开源项目、实战经验、高效开发东西等,您的重视将是我的更新动力!