多线程知识:三个线程如何交替打印ABC循环100次
本文博主给大家讲解一道网上非常经典的多线程面试题目。关于三个线程如何交替打印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次的更多相关文章
- Java多线程wait和notify协作,按序打印abc
有一个经典的多线程面试题:启三个线程,按序打印ABC 上代码: package cn.javaBase.study_thread1; class MyRunnable1 implements Runn ...
- 三个线程ABC,交替打印ABC
转载与:https://www.cnblogs.com/x_wukong/p/4009709.html 创建3个线程,让其交替打印ABC . 输出如下: ABCABCABCABC. 方法:使用syn ...
- 迅雷笔试题 (JAVA多线程)启动三个线程,分别打印A B C,现在写一个程序 循环打印ABCABCABC
题目:http://wenku.baidu.com/view/d66187aad1f34693daef3e8a.html 启动三个线程,分别打印A B C,现在写一个程序 循环打印ABCABCABC. ...
- Java 多线程(三)—— 线程的生命周期及方法
这篇博客介绍线程的生命周期. 线程是一个动态执行的过程,它也有从创建到死亡的过程. 线程的几种状态 在 Thread 类中,有一个枚举内部类: 上面的信息以图片表示如下: 第一张图: 第二张图:把等待 ...
- 多线程面试题之【三线程按顺序交替打印ABC的方法】
建立三个线程,线程名字分别为:A.B.C,要求三个线程分别打印自己的线程名字,但是要求三个线程同时运行,并且实现交替打印,即按照ABCABCABC的顺序打印.打印10轮,打印完毕控制台输出字符串:&q ...
- 斐讯面试记录—三线程交替打印ABC
package cn.shenzhen.feixun; public class PrintABC extends Thread{ private String name; private Objec ...
- python多线程交替打印abc以及线程池进程池的相关概念
import threading import sys import time def showa(): while True: lockc.acquire() #获取对方的锁,释放自己的锁 prin ...
- java多线程(三)线程的安全问题
1.1. 什么是线程安全 如果有多个线程同时运行同一个实现了Runnable接口的类,程序每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的:反之,则是线程不 ...
- Java线程和多线程(三)——线程安全和同步
线程安全在Java中是一个很重要的课题.Java提供的多线程环境支持使用Java线程.我们都知道多线程共享一些对象实例的话,可能会在读取和更新共享数据的事后产生数据不一致问题. 线程安全 之所以会产生 ...
- (Java多线程系列三)线程间通讯
Java多线程间通讯 多线程之间通讯,其实就是多个线程在操作同一个资源,但是操作的动作不同. 1.使用wait()和notify()方法在线程中通讯 需求:第一个线程写入(input)用户,另一个线程 ...
随机推荐
- 如何实现 Java SpringBoot 自动验证入参数据的有效性
Java SpringBoot 通过javax.validation.constraints下的注解,实现入参数据自动验证 如果碰到 @NotEmpty 否则不生效,注意看下 @RequestBody ...
- Ubuntu编译安装protobuf-3.6.1
一.下载源码包 下载源码URL:https://github.com/protocolbuffers/protobuf/releases/download/v3.6.1/protobuf-all-3. ...
- 分享一个开源的windows安卓投屏工具,scrcpy
看到scrcpy可能很多人会以为是大名鼎鼎的Scrcpy(一个十分强大的多线路爬虫框架),sorry今天分享的主角不是他,而是他: github地址:https://github.com/Genymo ...
- SkyWalking的学习之一
SkyWalking的学习之一 前言 最近在学习应用调优诊断等内容. 现在实际工作中实质上的拆分和微服务在售前阶段 所以真正用到链路的地方比较少. 但是人生都是要向前看的. 想着一方面提高自己. 一方 ...
- 《HelloGitHub》第 85 期
兴趣是最好的老师,HelloGitHub 让你对编程感兴趣! 简介 HelloGitHub 分享 GitHub 上有趣.入门级的开源项目. https://github.com/521xueweiha ...
- 武装你的WEBAPI-OData Versioning
本文属于OData系列 目录 武装你的WEBAPI-OData入门 武装你的WEBAPI-OData便捷查询 武装你的WEBAPI-OData分页查询 武装你的WEBAPI-OData资源更新Delt ...
- MySQL中的Join 的算法(NLJ、BNL、BKA)
本文已收录至Github,推荐阅读 Java随想录 微信公众号:Java随想录 目录 摘要 什么是Join Index Nested-Loop Join Block Nested-Loop Join ...
- 百度飞桨(PaddlePaddle)- 张量(Tensor)
飞桨 使用张量(Tensor) 来表示神经网络中传递的数据,Tensor 可以理解为多维数组,类似于 Numpy 数组(ndarray) 的概念.与 Numpy 数组相比,Tensor 除了支持运行在 ...
- 2022-09-12:以下go语言代码输出什么?A:true;B:false;C:无法编译;D:运行时 panic。 package main func main() { var x chan<-
2022-09-12:以下go语言代码输出什么?A:true:B:false:C:无法编译:D:运行时 panic. package main func main() { var x chan< ...
- 2021-01-31:Redis集群方案不可用的情况有哪些?
福哥答案2021-01-31:[答案来自此链接:](https://www.zhihu.com/question/442112697)一个集群模式的官方推荐最小最佳实践方案是 6 个节点,3 个 Ma ...