共计 8053 个字符,预计需要花费 21 分钟才能阅读完成。
本文博主给大家解说一道网上十分经典的多线程面试题目。对于三个线程如何交替打印 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 地址:https://github.com/wayn111/waynboot-mall
解决思路
这是一个典型的多线程同步的问题,须要保障每个线程在打印字母之前,可能判断是否轮到本人执行,以及在打印字母之后,可能告诉下一个线程执行。为了实现这一指标,博主讲介绍以下 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】每周分享技术干货、开源我的项目、实战经验、高效开发工具等,您的关注将是我的更新能源!
本文参加了 SegmentFault 思否写作挑战「摸索编码世界之旅 – 记我的第一份编程工作」,欢送正在浏览的你也退出。