在两道多线程基础题“顺序打印”中对比一下Java中的wait()和join()

news2024/10/7 15:30:40

目录

一、基础

二、进阶


一、基础

有三个线程,线程名称分别为:a,b,c,每个线程打印自己的名称。

需要让他们同时启动,并按 c,b,a的顺序打印。

这道题要求打印 cba,且只打印一次。如何保证线程 cba 的执行顺序?容易想到,只需要让这三个线程按一定顺序串行执行即可,采用 join() 就可以轻易做到。

join() 的作用是,让当前线程等待调用 join() 的线程执行完毕后,再继续往下执行。在使用 `join()` 方法时,调用线程会进入等待状态,直到被等待的线程执行完毕。在 Java 中,可以通过 `join()` 方法来实现线程之间的同步。例如,在一个多线程程序中,如果需要让线程 A 在线程 B 执行完后再继续执行,可以在线程 A 中调用线程 B 的 `join()` 方法。这样,线程 A 会等待线程 B 终止后再继续执行。

public class Test {
    public static void main(String[] args) throws InterruptedException {

        Thread t1 = new Thread(() -> {
            System.out.print("A");
        });

        Thread t2 = new Thread(() -> {
            System.out.print("B");
        });

        Thread t3 = new Thread(() -> {
            System.out.print("C");
        });

        t3.start();
        t3.join();
        t2.start();
        t2.join();
        t1.start();
        t1.join();
    }
}


二、进阶

有三个线程,分别只能打印A,B和C。要求按顺序打印ABC,打印10次。

输出示例:

ABC

ABC

ABC

ABC

ABC

ABC

ABC

ABC

ABC

ABC 

这道题的特点在循环打印。循环打印的情况下,join方法就不那么适用了。因为在这个场景中,线程 A、B、C 需要循环执行多次,如果在每次执行结束后都使用 `join()` 方法等待另外两个线程执行完毕后再继续执行,会导致线程的阻塞和唤醒操作频繁地发生,从而影响程序的性能。

况且,实现起来也并不那么直接。以下的几种做法都是不正确的:

错误方法-1:该代码实际上只能打印出一次ABC。因为join是等待线程完全终止。虽然有多次循环,但实际上只有第一次循环执行时,启动了线程;后面线程终止后,就没有再启动过了

错误方法-2:会报线程状态异常。因为在同一线程中,反复多次start()了同一线程。

相比之下,在这个场景中,使用 wait() 和 notifyAll() 方法可以更好地实现线程之间的同步和协作。

使用 wait() 和 notifyAll() 方法可以让线程在需要等待的时候进入等待状态,直到满足某个条件后再唤醒线程

思路如下:

创建了一个 PrintABC 类,其中包含了打印A、B、C的三个方法,以及控制打印顺序的状态值 count 和用于线程间通信的锁对象 locker

在每个打印方法中,使用了一个 while 循环来判断是否轮到该线程打印。为什么使用while而不是if,这点后面再说。该程序中,如果不是自己打印的轮次,则调用 wait() 方法使线程等待,否则进行打印操作。

在打印完成后,将count加1,并调用 notifyAll() 方法通知其他线程。也即,每次有一个线程执行了打印过之后,就要把所有线程都唤醒,让它们再判断一次是否轮到自己打印了。

最后,在 main() 方法中创建三个线程并启动它们,分别调用打印A、B、C的方法。执行程序后,即可按顺序打印10次ABC。

class PrintABC {
    static final Object locker = new Object();    // 锁对象
    static int count;    // 状态值,用于控制打印顺序


    public static void printA() throws InterruptedException {
        synchronized (locker) {
            for (int i = 0; i < 10; i++) {
                while(count % 3 != 0) {    // 判断是否轮到该线程打印
                    locker.wait();
                }

                count++;    // 状态值加1,并通知其他线程
                System.out.print("A");
                locker.notifyAll();
            }
        }
    }
    public static void printB() throws InterruptedException {
        synchronized (locker) {
            for (int i = 0; i < 10; i++) {
                while(count % 3 != 1) {
                    locker.wait();
                }

                count++;
                System.out.print("B");
                locker.notifyAll();
            }
        }
    }
    public static void printC() throws InterruptedException {
        synchronized (locker) {
            for (int i = 0; i < 10; i++) {
                while(count % 3 != 2) {
                    locker.wait();
                }

                count++;
                System.out.println("C");
                locker.notifyAll();
            }
        }
    }
}
public class Test {
    public static void main(String[] args) {

        Thread t1 = new Thread(() -> {
            try {
                PrintABC.printA();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        });

        Thread t2 = new Thread(() -> {
            try {
                PrintABC.printB();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        });

        Thread t3 = new Thread(() -> {
            try {
                PrintABC.printC();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        });


        t1.start();
        t2.start();
        t3.start();
    }
}

Q:为什么要notifyAll,而不是notify?

A:在上面的Java代码示例中,我们使用 notifyAll() 方法来通知其他线程,而不是使用 notify() 方法。这是因为在有多个线程正在 wait 时, notify() 方法只会随机唤醒一个等待该对象锁的线程,而 notifyAll() 方法会唤醒所有等待该对象锁的线程。由于我们希望所有等待线程都能被唤醒并进行状态判断,因此使用 notifyAll() 更为合适。

Q:为什么要用while(count % 3 != 0)而不是if(count % 3 != 0)?

A:在多线程编程中,使用 while 循环来判断条件是否满足,通常是为了避免虚假唤醒的问题。

什么是虚假唤醒?贴一个大佬的总结:Java线程虚假唤醒是什么、如何避免?

多线程环境下,有多个线程执行了wait()方法,需要其他线程执行notify()或者notifyAll()方法去唤醒它们;假如多个线程都被唤醒了,但是只有其中一部分是有用的唤醒操作,其余的唤醒都是无用功;对于不应该被唤醒的线程而言,是虚假唤醒。 

换句话说,虽然notifyAll()把所有线程都喊醒了,但该程序中要求最终只有一个线程是能继续执行的;其它线程还得wait();那么醒来的这个“其它线程”,就是虚假唤醒。

在本例中,我们希望三个线程分别打印字母 A、B、C,且每个线程只能打印一种字母。为了实现这个目标,我们引入了一个状态变量 count,表示当前可以打印的字母是哪个线程负责打印的。具体来说,当 count 的值为 0、1、2 时,分别表示线程 A、B、C 可以打印字母;当 count 的值为 3、4、5 时,表示线程 A、B、C 分别已经打印完了一次字母,需要等待其他线程打印完后才能再次打印。

比如说,此时count是1。那么一开始,三个线程其实都会被唤醒,然后判断当前自己应不应该打印字母。但是此时只有B是可以被打印的,A和C就是虚假唤醒。如果是if,那if进行了一次判断后,A和C依旧进入wait。然而,等到B执行完后,再次notifyAll,A和C在醒来之后不会再有第二次条件判断,而是直接在wait这行代码处被唤醒并接着向下执行了,A和C会同时执行打印操作。这样就会打乱打印顺序。

if-运行结果

为了避免这种情况,我们使用 while 循环来判断条件是否满足。在使用 while 循环时,线程会在被唤醒后再次检查条件是否满足,如果不满足则继续等待,从而避免了虚假唤醒的问题。 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/517660.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

开发中proc文件系统的简单使用

使用proc文件系统 文章目录 使用proc文件系统1.meminfo文件2. free命令3、创建 /proc 节点4、使用 file_operations 实现 proc 文件读写 导向内核信息5、使用 seq_file 实现 proc 文件的读取 在Linux系统中&#xff0c; “/proc”文件系统十分有用&#xff0c; 它被内核用于向用…

可视化图表API格式要求有哪些?Sugar BI详细代码示例(4)

Sugar BI中的每个图表可以对应一个数据 API&#xff0c;用户浏览报表时&#xff0c;选定一定的过滤条件&#xff0c;点击「查询」按钮将会通过 API 拉取相应的数据&#xff1b;前面说过&#xff0c;为了确保用户数据的安全性&#xff0c;Sugar BI上的所有数据请求都在Sugar BI的…

进击数据基础设施新蓝海,厂商如何“择木而栖”?

文 | 螳螂观察 作者 | 李永华 多样的应用需求&#xff0c;倒逼底层存储能力不断升级&#xff1b; 复杂的数据状况&#xff0c;要求存储能够“兼容并蓄”&#xff1b; 客户企业在数字化方面的战略转型升级&#xff0c;总是触及到存储…… 当数据基础设施成为新的蓝海&#…

大学计算机基础-题库刷题-精选

题库刷题&#xff1a; 写在前面&#xff1a; 这个是我准备应对学校转专业考试而刷的题库&#xff0c; 也是大学计算机的题库&#xff0c;同样适用于大学计算机这门课的期末考试。 精选了一些重要的题目。 目录 题库刷题&#xff1a; 写在前面&#xff1a; 题目1&#x…

内卷时代,大厂产品经理仅用3步破局

本文首发自「慕课网」&#xff0c;想了解更多IT干货内容&#xff0c;程序员圈内热闻&#xff0c;欢迎关注"慕课网"&#xff01; 作者&#xff1a;申悦|慕课网讲师 在当下互联网环境下&#xff0c;产品经理究竟要如何破局&#xff1f; 我认为&#xff0c;既然要破局…

RocketMQ学习笔记:生产者Producer

DefaultMQProducer 根据上文&#xff1a;RocketMQ学习笔记&#xff1a;消息Message - 掘金 (juejin.cn)&#xff0c;我们定位到Producer中的这一行代码&#xff1a; java 复制代码 DefaultMQProducer producer new DefaultMQProducer("ProducerGroupName"); pro…

2023年3月GESP能力等级认证C++一级真题

一、单选题&#xff08;每题2分&#xff0c;共30分&#xff09; 1.以下不属于计算机输入设备的有&#xff08;B &#xff09;。(2分) A&#xff0e;键盘 B&#xff0e;音箱 C&#xff0e;鼠标 D&#xff0e;传感器 2.计算机系统中存储的基本单位用 B 来表示&#xff0c;它…

Git 常用命令笔记

下载安装这里就不赘述了&#xff0c;直接下一步就行&#xff01; 一、常用命令 1. 增加删除/文件 添加当前目录的所有文件到暂存区 git add .添加指定文件到暂存区 git add [file1] [file2] ...添加指定目录到暂存区&#xff0c;包括子目录 git add [dir]对于同一个文件的多…

多种方法解决There is no tracking information for the current branch的错误

文章目录 1. 复现错误2. 分析错误3. 解决错误3.1 远程有分支3.2 远程无分支 4. 总结 1. 复现错误 今天发布某版本的项目&#xff0c;准备创建个v0point1分支&#xff0c;后期如果修改该版本&#xff0c;直接在该分支上修改即可。 首先&#xff0c;使用git branch v0point1命令…

问道游戏私人服务器架设+详细搭建教程+外网教程

搭建条件: 1、服务器一台, 2、下载服务端 搭建教程&#xff1a; 1.先安装宝塔 2、放行安全组的相应端口 具体要放行的端口有&#xff1a;3306、888、8888、5000、8101、8110、8120、8160-8168&#xff08;这个是范围之8160是一线&#xff0c;依次类推&#xff09; 3、安装数据库…

别点了!CAS登录对接,这个Bug让你反复登录!

目录 引言 背景描述 问题描述 问题排查 软件测试工程师发展规划路线 引言 你是否曾经在登录一个网站时&#xff0c;不断输入账号密码&#xff0c;却发现自己总是无法成功登录&#xff1f;或者你是否曾经遇到过跨域问题导致的登录失败&#xff1f; 今天我要和大家分享的就…

Speech and Language Processing之神经网络

上面这句话很好的解释了一件事&#xff0c;就是“大力出奇迹” &#xff0c;当神经元的数目足够足够多的时候&#xff0c;机器所能做到的事情就很复杂、很难理解了&#xff0c;这是不是说明chatgpt的成功也是因为大&#xff1f; 现代神经网络是一个由小型计算单元组成的网络&am…

前端 Web 性能清单

&#x1f482; 个人网站:【海拥】【摸鱼游戏】【神级源码资源网】&#x1f91f; 前端学习课程&#xff1a;&#x1f449;【28个案例趣学前端】【400个JS面试题】&#x1f485; 想寻找共同学习交流、摸鱼划水的小伙伴&#xff0c;请点击【摸鱼学习交流群】 提高 Web 应用程序的性…

HS6621系列低功耗国产蓝牙芯片 支持蓝牙5.1

HS6621CxC是一个功耗优化的蓝牙低功耗和专有的2.4 ghz应用真正的芯片上系统(SOC)解决方案。它集成了一个具有蓝牙基带和丰富外设的低功耗射频收发器I0扩展。HS6621CxC还集成了电源管理&#xff0c;提供高效率电源管理。它的目标是2.4 G蓝牙低功耗系统&#xff0c;人机界面设备(…

尚无忧【已对接硬件】共享自习室,共享麻将馆,共享茶室,共享空间,共享台球室,共享健身房无人值thinkphp开发

1、定位功能&#xff1a;可定位附近是否有店 2、能通过关键字搜索现有的店铺 3、个性轮播图展示&#xff0c;系统公告消息提醒 4、个性化功能展示&#xff0c;智能排序&#xff0c;距离、价格排序 5、现有店铺清单展示&#xff0c;订房可查看房间单价&#xff0c;根据日期、…

面试了一位6年的软件测试,一问三不知,他还反怼我...

最近看了很多简历&#xff0c;很多候选人年限不小&#xff0c;但是做的都是一些非常传统的项目&#xff0c;想着也不能通过简历就直接否定一个人&#xff0c;何况现在大环境越来 越难&#xff0c;大家找工作也不容易&#xff0c;于是就打算见一见。 在沟通中发现&#xff0c;由…

linux 修改 /etc/locale.conf无效问题处理办法

问题背景&#xff1a; 我在做测试系统文档转换成其他格式文档时&#xff0c;按照系统要求配置系统的编码格式为&#xff1a;utf-8 但是 尤其是设置&#xff1a;LC_ALLZh_CN.UTF- 8 但是 即使 我已经设置了 /etc/locale.conf内容如下&#xff1a; 并且source /etc/locale.con…

【FPGA-DSP】第十期:sysgen算法封装与调用

参考视频教程第10期 - sysgen算法封装与调用 - 基于FPGA的数字信号处理系统开发笔记_哔哩哔哩_bilibili 该教程主要实现如何将sysgen编写的算法模块给实际的应用起来 添加封装有两种方式&#xff1a; 在Vivado中使用ip核添加算法模块封装在Sysgen中将算法模块封装 Sysgen开发…

为何使用 B+ 树而非二叉查找树或 B 树做索引?

二叉树 B-Tree BTree 一、为何使用 B 树而非二叉查找树做索引&#xff1f; 我们知道二叉树的查找效率为 O(logn)&#xff0c;当树过高时&#xff0c;查找效率会下降。另外由于我们的索引文件并不小&#xff0c;所以是存储在磁盘上的。 文件系统需要从磁盘读取数据时&#xff0c…

【Vue工程】010-UnoCSS 即时按需原子 CSS 引擎

【Vue工程】010-UnoCSS 即时按需原子 CSS 引擎 文章目录 【Vue工程】010-UnoCSS 即时按需原子 CSS 引擎一、概述1、简介2、官网 二、基本使用1、安装2、修改 vite.config.ts3、根目录创建 uno.config.ts4、在 main.ts 中引入5、VS Code 安装 UnoCSS 插件6、在组件中使用7、访问…