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

这篇具有很好参考价值的文章主要介绍了在两道多线程基础题“顺序打印”中对比一下Java中的wait()和join()。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

目录

一、基础

二、进阶


一、基础

有三个线程,线程名称分别为: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();
    }
}

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


二、进阶

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

输出示例:

ABC

ABC

ABC

ABC

ABC

ABC

ABC

ABC

ABC

ABC 

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

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

在两道多线程基础题“顺序打印”中对比一下Java中的wait()和join()
错误方法-1:该代码实际上只能打印出一次ABC。因为join是等待线程完全终止。虽然有多次循环,但实际上只有第一次循环执行时,启动了线程;后面线程终止后,就没有再启动过了
在两道多线程基础题“顺序打印”中对比一下Java中的wait()和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会同时执行打印操作。这样就会打乱打印顺序。

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

为了避免这种情况,我们使用 while 循环来判断条件是否满足。在使用 while 循环时,线程会在被唤醒后再次检查条件是否满足,如果不满足则继续等待,从而避免了虚假唤醒的问题。 文章来源地址https://www.toymoban.com/news/detail-441236.html

到了这里,关于在两道多线程基础题“顺序打印”中对比一下Java中的wait()和join()的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 打印的前后顺序

    面试题经常会有 宏任务:script(整体代码),setTimeout,UI渲染,VO,postMessage,MessageChannel,setlmmedate(node环境) 微任务:Promise, MultationObserver, proxess.nextTick(node环境) 执行顺序 1 3  4  6  hhhhhh  5 2 注意:  Promise ,一旦创建立即执行,打印7是迷惑项不会执行

    2024年01月23日
    浏览(27)
  • 简单对比一下 C 与 Go 两种语言

    以下内容为本人的学习笔记,如需要转载,请声明原文链接 微信公众号「ENG八戒」https://mp.weixin.qq.com/s/U6jIT837x5Yxe6Ev1aMDsA 使用一个简单的计数程序将古老的 C 语言与现代 Go 进行比较。 Go 是一种现代编程语言,追溯其历史大部分源自编程语言 C。所以,任何熟悉 C 语言的开发者

    2024年02月11日
    浏览(41)
  • 带你认识一下多模态对比语言图像预训练CLIP

    本文分享自华为云社区《多模态对比语言图像预训练CLIP:打破语言与视觉的界限》,作者:汀丶。 一种基于多模态(图像、文本)对比训练的神经网络。它可以在给定图像的情况下,使用自然语言来预测最相关的文本片段,而无需为特定任务进行优化。CLIP的设计类似于GPT

    2024年02月06日
    浏览(38)
  • 算法进阶——按之字形顺序打印二叉树

    题目 给定一个二叉树,返回该二叉树的之字形层序遍历,(第一层从左向右,下一层从右向左,一直这样交替)。 数据范围:0≤n≤1500,树上每个节点的val满足∣val∣=1500 要求:空间复杂度:O(n),时间复杂度:O(n) 例如: 给定的二叉树是{1,2,3,#,#,4,5} 该二叉树之字形层序遍历

    2024年01月21日
    浏览(44)
  • 从性能、开发难度、推广使用等方面,对比一下django flask fastapi的优点和缺点

    当选择一个适合你的项目的Python Web框架时,你可能会考虑以下几个方面:性能、开发难度和推广程度。在这里,我们将比较Django、Flask和FastAPI这三个常用的框架,深入探讨它们的优点和缺点。 1. 性能:    - Django:Django是一个全功能的框架,它提供了许多内置的功能和扩展。

    2024年02月12日
    浏览(42)
  • 宏任务与微任务执行顺序及对比记录

    目录 前言 一、 宏任务、微任务的基本概念 1.宏任务介绍 2.微任务介绍 3.宏任务、微任务发展及出现的原因:  4.宏任务、微任务的基本类型 二、 事件循环模型(Event Loop) 三、 Promise、async和await 在事件循环中的处理 1.Promise:         2. async/await : 这个知识点比较容易忽

    2024年01月21日
    浏览(47)
  • JS面试题:说一下js单线程的理解?

        js是单线程的,内部要处理的任务分同步任务、异步任务     异步任务分微任务、宏任务     执行顺序:【又称 事件循环机制 】     先执行同步任务,遇到异步宏任务则将异步宏任务放入宏任务队列中,遇到异步微任务则将异步微任务放入微任务队列中。当所有同步任

    2024年01月25日
    浏览(32)
  • 详细说明一下Java中进程和线程的区别和联系

    Java中的进程和线程都是操作系统中执行代码的概念,但它们有以下区别: 进程是资源分配的最小单位,而线程是程序执行的最小单位。每个进程都有自己独立的内存空间、文件句柄等资源,而线程共享进程的资源。 进程之间相互独立,一个进程崩溃不会影响其他进程,而线

    2024年02月09日
    浏览(57)
  • 多线程知识:三个线程如何交替打印ABC循环100次

    本文博主给大家讲解一道网上非常经典的多线程面试题目。关于三个线程如何交替打印ABC循环100次的问题。 下文实现代码都基于Java代码在单个JVM内实现。 给定三个线程,分别命名为A、B、C,要求这三个线程按照顺序交替打印ABC,每个字母打印100次,最终输出结果为: 推荐博

    2024年02月12日
    浏览(99)
  • 尝试一下Guava带返回值的多线程处理类ListenableFuture

    最近在学习,Java实现异步编程的8种方式这篇博客的时候,没有找到比较好的一个学习demo,故在此整理一下。 ListenableFuture是Java中的一个接口,它继承自java.util.concurrent.Future接口。ListenableFuture主要用于异步计算场景,允许在计算完成后添加回调函数(Callback),从而实现对异

    2024年02月13日
    浏览(47)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包