本文博主给大家讲解一道网上非常经典的多线程面试题目。关于三个线程如何交替打印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】每周分享技术干货、开源项目、实战经验、高效开发工具等,您的关注将是我的更新动力!

多线程知识:三个线程如何交替打印ABC循环100次的更多相关文章

  1. Java多线程wait和notify协作,按序打印abc

    有一个经典的多线程面试题:启三个线程,按序打印ABC 上代码: package cn.javaBase.study_thread1; class MyRunnable1 implements Runn ...

  2. 三个线程ABC,交替打印ABC

    转载与:https://www.cnblogs.com/x_wukong/p/4009709.html 创建3个线程,让其交替打印ABC . 输出如下:  ABCABCABCABC. 方法:使用syn ...

  3. 迅雷笔试题 (JAVA多线程)启动三个线程,分别打印A B C,现在写一个程序 循环打印ABCABCABC

    题目:http://wenku.baidu.com/view/d66187aad1f34693daef3e8a.html 启动三个线程,分别打印A B C,现在写一个程序 循环打印ABCABCABC. ...

  4. Java 多线程(三)—— 线程的生命周期及方法

    这篇博客介绍线程的生命周期. 线程是一个动态执行的过程,它也有从创建到死亡的过程. 线程的几种状态 在 Thread 类中,有一个枚举内部类: 上面的信息以图片表示如下: 第一张图: 第二张图:把等待 ...

  5. 多线程面试题之【三线程按顺序交替打印ABC的方法】

    建立三个线程,线程名字分别为:A.B.C,要求三个线程分别打印自己的线程名字,但是要求三个线程同时运行,并且实现交替打印,即按照ABCABCABC的顺序打印.打印10轮,打印完毕控制台输出字符串:&q ...

  6. 斐讯面试记录—三线程交替打印ABC

    package cn.shenzhen.feixun; public class PrintABC extends Thread{ private String name; private Objec ...

  7. python多线程交替打印abc以及线程池进程池的相关概念

    import threading import sys import time def showa(): while True: lockc.acquire() #获取对方的锁,释放自己的锁 prin ...

  8. java多线程(三)线程的安全问题

    1.1. 什么是线程安全 如果有多个线程同时运行同一个实现了Runnable接口的类,程序每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的:反之,则是线程不 ...

  9. Java线程和多线程(三)——线程安全和同步

    线程安全在Java中是一个很重要的课题.Java提供的多线程环境支持使用Java线程.我们都知道多线程共享一些对象实例的话,可能会在读取和更新共享数据的事后产生数据不一致问题. 线程安全 之所以会产生 ...

  10. (Java多线程系列三)线程间通讯

    Java多线程间通讯 多线程之间通讯,其实就是多个线程在操作同一个资源,但是操作的动作不同. 1.使用wait()和notify()方法在线程中通讯 需求:第一个线程写入(input)用户,另一个线程 ...

随机推荐

  1. LeeCode 栈与队列问题(二)

    LeeCode 239: 滑动窗口最大值 题目描述 给你一个整数数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧.你只可以看到在滑动窗口内的 k 个数字.滑动窗口每次只向右 ...

  2. Nvidia GPU池化-远程GPU

    1 背景 Nvidia GPU得益于在深度学习领域强大的计算能力,使其在数据中心常年处于绝对的统治地位.尽管借助GPU虚拟化实现多任务混布,提高了GPU的利用率,缓解了长尾效应,但是GPU利用率的绝对 ...

  3. jquery实现一个网页同时调用多个倒计时

    <div class="time countdown_1" data-time="1449429731"> <span class=" ...

  4. 开心档之MySQL WHERE 子句

    MySQL WHERE 子句 我们知道从 MySQL 表中使用 SQL SELECT 语句来读取数据. 如需有条件地从表中选取数据,可将 WHERE 子句添加到 SELECT 语句中. 语法 以下是 ...

  5. 关于Java中代码的执行顺序

    结论 注意 只有显式的加载类 JVM才会加载到内存中 先加载父类的静态代码块 然后执行子类静态代码块 当前类存在类静态变量注意引用类型没进行赋值操作初始化为null 并不会显式的加载类又存在静态代码块 ...

  6. 笔记:C++学习之旅 ---string 类、vector和迭代器

    string 类 #include <iostream> #include <string> using namespace std; int main() {         ...

  7. 2021牛客OI赛前集训营-提高组(第三场) 第二题 交替 题解与结论证明

    题目描述 一个长度为 \(n\) 的数组\(A\),每秒都会变成一个长度为 \(n − 1\) 新数组 \(A'\),其变化规 则如下: 若当前数组 \(A\) 的长度 \(n\) 为偶数,则对于新数 ...

  8. 简单理解重载运算符&位运算

    重载运算符 作用 重载运算符的作用大致可以理解为自定义一个运算法则,比如当我们在使用结构体的时候,我们有时候会用到优先队列,但是优先队列并不能对于结构体使用,所以这个时候我们就需要用到重载运算符来自定 ...

  9. VS2022使用ClickOnce发布程序本地安装.net框架

    因为遇到下面的错误,没有在网上搜到详细解决问题的教程,费了一些时间才解决了问题,特此记录一下,也希望能帮助到其他人. 要在"系统必备"对话框中启用"从与我的应用程序相同的 ...

  10. P8936 月下缭乱 Sol

    考虑对操作的区间 \([l_i,r_i]\) 的下标进行扫描线而不是对操作的值扫.用 \(m\) 个 set 动态维护 \(x_i\) 对应的操作的下标集合,再用一个可删堆来维护当前所有操作 \(x_ ...