面试官:为什么有了sleep还需要wait?

news2024/11/25 16:39:29

1. 能不能调整线程先后顺序?

对于线程执行最大的问题就是随机调度,抢占式执行,对于程序猿来讲,是不喜欢这种随机性的,程序猿喜欢确定的东西,于是就有了一些方法,可以控制线程之间的执行顺序,虽然线程在内核里调度是随机的,但我们可以通过一些 api 让线程主动阻塞等待,主动放弃 CPU 给其他线程让路呀!

就比如说,在地铁上,张三看到一位老人上地铁了,主动让座,老人坐了一会,起身对小伙说,我还有一站就到了,你来坐着吧,我站一会就下车了。

这是不是就像线程1正在占用 CPU 资源了,突然线程2开始工作了,于是线程1就让线程2先去工作,等线程2工作差不多了,在通知线程1可以工作了。

在实际开发中,很多时候线程之间是需要相互配合的。

比如篮球哥喜欢打篮球,篮球里,一个队伍五个人,小前锋,大前锋,中锋,后卫,分位,这 5 个角色就像 5 个线程,如果这 5 个人都争这一个球,那这个的队伍就没有配合性,必定会输球。

如果这 5 个人打好配合,先谁持球,然后接着执行什么战术,有合理的战术安排,此时球就能很好的在这 5 个人的手里运作起来,进球的概率也就大大提升。

再比如,球员a 先持球过半场,传球给球员b,球员b接球就投,球进了!

此时是不是就需要 a 先拿球过半场啊,等 a 过了半场,在传球给 b ,在 a 没有传球之前 b 是不能拿到球的!

也就是线程1没有执行到一定阶段,线程2是不能工作的!

对于完成上述的配合操作,主要涉及到三个方法:

  • wait() / wait(long timeout)

  • notify / notiryAll()

此处的方法都是 Object 类中的方法,Object 类是所有类的父类,所以所有对象都有上述方法。

后续的内容也是围绕上述方法进行展开。


2. wait 方法

当某个对象调用 wait 方法时,wait 会做如下三件事:

  • wait 使当前执行代码的线程进行等待(把线程放到阻塞队列中)

  • 释放当前的锁

  • 满足一定条件时被唤醒,重新尝试获取这个锁

由于 wait 执行时会释放当前的锁,所以调用 wait 的时候需要先获取到锁,即 wait 需要搭配 synchronized 使用。

wait 的结束条件(满足一个即可):

  • 其他线程调用该对象的 notify 方法

  • wait 超时等待(wait 方法提供一个带有 timeout 参数的版本, 来指定等待时间)

  • 其他线程调用该线程的 interrupted 方法,导致 wait 抛出 InterruptedException 异常

public static void main(String[] args) {
    Object object = new Object();
    Thread t1 = new Thread(() -> {
        synchronized (object) {
            System.out.println("开始等待!!!");
            try {
                object.wait();
                System.out.println("等待结束!!!");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    });
    t1.start();
}

显然上述这个代码是一个 "死等",因为没有触发上述 wait 结束条件的任意一个,所以 t1 线程会无止境的等待下去:

通过 jconsole 工具也能发现,t1 线程始终处于 WAITING 状态!

如何让 wait 结束,那么只要满足上述所说的三个 wait 结束条件即可。


3. notify 方法

notify 的作用是唤醒等待的线程

  • notify 这个方法也要在同步代码块或同步方法中执行(被synchronized 修饰),notify 用于通知哪些可能等待该对象锁的其他线程,并使他们重新获取该对象的锁。

  • 如果有多个线程等待该对象的锁, 则由线程调度器随机挑选出一个呈 wait 状态的线程,并不会采取先来后到的机制。

  • notify 方法后,当前线程不会马上释放该对象锁,要等到执行完 notify 所处被 synchronized 修饰的代码块执行结束,才能释放对象锁!

注意,通过指定对象调用 wait() 进入 WAITING 状态的线程,只有指定对象调用 notify 唤醒后(特殊情况除外),该线程才能尝试获取锁,接着往下执行!

notify 就好像一个妈妈(指定对象),妈妈手上拿着一块小蛋糕,有三个小朋友在桌子旁边坐着等(妈妈.wait),妈妈随机喊了一个小朋友,让他来吃蛋糕(妈妈.notify),但是妈妈并没有把蛋糕放下(没有结束对应代码块,也就是还未释放锁),当妈妈把蛋糕放在桌子上(锁被释放),这个小朋友才能去吃蛋糕(获取到锁)。

此时有了上述知识,我们就可以实现下上述图中吃蛋糕的场景了(为了代码简洁,我们只设定两个线程来等待被唤醒):

public static void main(String[] args) throws InterruptedException {
    Object object = new Object();
    Thread t1 = new Thread(() -> {
        synchronized (object) {
            System.out.println("张三进入 WAITING 状态");
            try {
                object.wait();
                System.out.println("张三吃到蛋糕了!");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    });

    Thread t2 = new Thread(() -> {
        synchronized (object) {
            System.out.println("李四进入 WAITING 状态");
            try {
                object.wait();
                System.out.println("李四吃到蛋糕了!");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    });

    t1.start();
    t2.start();
    Thread.sleep(10); // 保证两个线程都进入到 WAITING 状态
    synchronized (object) {
        object.notify();
    }
}

可能大家多次测试上述代码后,发现一直都是张三吃到了蛋糕啊,但是其实这个是随机的,因为 CPU 就是随机调度的,这个咱们就没必要钻牛角尖了,实在要钻,可以创建线程池(后续讲),搞一堆线程进行测试即可。

此时问题来了,当释放锁了之后,也就是妈妈把蛋糕放在桌子上了,此时被唤醒的线程是可以去拿蛋糕的,但是有没有可能释放锁的瞬间,被其他处在 RUNNABLE 状态的线程给劫持了呢?(其他线程也来竞争这把锁) 也就是突然冲进来了一条小狗,把蛋糕给抢到了,其实是有这种情况的:

public static void main(String[] args) throws InterruptedException {
    Object object = new Object();
    Thread t1 = new Thread(() -> {
        synchronized (object) {
            System.out.println("张三进入 WAITING 状态");
            try {
                object.wait();
                System.out.println("张三吃到蛋糕了!");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    });

    Thread t2 = new Thread(() -> {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        synchronized (object) {
            System.out.println("小狗把蛋糕抢走了!");
            while (true) {} // 吃蛋糕
        }
    });

    t1.start();
    t2.start();
    Thread.sleep(1000); // 保证两个线程都进入到 WAITING 状态
    synchronized (object) {
        object.notify();
        Thread.sleep(1000); // 唤醒 t1 但并没有立即释放锁, 休眠 1s 再释放
    }
}

上述代码 main 线程等待 1000 毫秒后唤醒 t1 线程,此时 t1 被唤醒,就会重新尝试获取 object 对象锁,但是 t2 休眠了 1000 毫秒后,也想获取 object 对象锁。

唤醒 t1 之后,过了 1000 毫秒,也就意味着锁被释放,此时 t1 和 t2 都想获取到 object 对象锁,那究竟谁能获取到呢?这完全是随机的!比如下面的测试结果:

所以是有可能别半路截胡的,罪魁祸首还是因为随机调度,抢占式执行呀!所以以后在写多线程代码的时候一定要多多注意,要让每种执行顺序得到的结果都是一样的,这才是好的代码!

关于 notifyAll :

notifyAll 和 notify 非常相似,假设 5 个线程等待 object 对象唤醒,然后 object.notifyAll(),就会将这 5 个线程全部唤醒,然后这 5 个线程竞争 object 对象锁,没竞争到的,就继续进入 WAITING 状态。


4. 使用 wait 和 notify 注意点

一定要弄清楚是谁在等被谁唤醒!

如果 t1 里面调用 o1.wait(),那么只有其他线程调用了 o1.notify() 才能唤醒 t1,如果是其他线程调用 o2.notify(),是不能唤醒 t1 的!因为 t1 线程是在等 o1 唤醒!

而 o1 也只能唤醒在等他的线程,比如 t3 在等 student 唤醒,那调用 o1.notify() 是不能唤醒 t3 的,只能调用 studnet.notify() 才能唤醒 t3。

归根到底,我们一定要弄清楚,线程在等谁,也要弄清楚,这个对象,有哪些线程在等他的唤醒!

这里举两个例子来演示一下:

public static void main(String[] args) throws InterruptedException {
    Object o1 = new Object();
    Object o2 = new Object();
    Thread t = new Thread(() -> {
        synchronized (o1) {
            try {
                o1.wait();
                System.out.println("t 线程被 main 线程唤醒!");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    });
    t.start();
    Thread.sleep(10); // 保证 t 线程进入 WAITING 状态
    synchronized (o2) {
        o2.notify();
        System.out.println("执行完 o2.notify!");
    }
}

这段代码,t 线程在等待 o1 对象唤醒,所以 main 线程中 o2.notify() 是在唤醒等待 o2 的线程,显然没有线程在等待 o2 唤醒,所以空打一枪,然而 t 线程仍然处在 WAITING 状态。

如果对应对象 notify 的时候,没有线程在等待这个对象唤醒呢?那么就是无效唤醒,也没有什么副作用,所以我们以后写代码的时候还是要尽量保证先执行 wait 在执行 notify 才是有意义的,也就是在 notify 的时候,有线程在等待这个对象唤醒!


5. wait 带参数和 sleep 的区别

wait 的带参数版本,指定了最大的等待时间,看起来和 sleep 有点像,但是还是有本质区别的。

  • notify 唤醒 wait 的时候,是不会有任何异常的(正常的业务逻辑)

  • interrupt 唤醒 sleep 的时候,则是会抛出一个异常(表示逻辑出现了问题)

其实从理论上,wait 和 sleep 是没得比的,wait 是线程之间的通信,互相配合,而 sleep 是单纯让线程阻塞一段时间,唯一的相同点就是都可以让线程放弃 CPU 的调度一段时间而已。

  • wait 是需要搭配 synchronized 使用的,sleep 则不需要

  • wait 是 Object 的方法,而 sleep 是 Thread 的静态方法


下期预告:【多线程】单例模式

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

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

相关文章

Keil 手动添加自己的lib库

我是在做一个项目看蓝牙模块官方Demo程序时发现对库的使用, 库大家都知道是不需要编译的,而且别人是无法看到源代码的,这样的好处就是编译快,并且方便移植,更安全。 我的制作步骤如下: 1、首先要把整个工程…

大量电脑桌面文件一键批量复制移动

电脑桌面上的文件越来越多,处理起来越来越繁琐。如果需要把这些文件复制或移动到另一个文件夹中,手动一个个复制或移动不仅费时费力,还容易出错。那么有没有什么方法可以快速批量复制和移动这些文件呢?当然有!今天小编…

若依前后端分离如何动态渲染echarts图表

本章教程,主要介绍一下如何在若依前后端分离框架中,进行echarts动态渲染数据。 ECharts是一款基于JavaScript的数据可视化图表库,提供直观,生动,可交互,可个性化定制的数据可视化图表。ECharts最初由百度团队开源,并于2018年初捐赠给Apache基金会,成为ASF孵化级项目。…

027python-ddt

在unittest中,测试用例是不能传self以外的参数的,否则会报错import unittestclass TeatMath(unittest.TestCase):# def test_add(self,q): # TypeError: TeatMath.test_add() missing 1 required positional argument: q——>测试用例传参报错def t…

【KD-Tree】基于k-d树的KNN算法实现

文章目录 一、什么是KD-Tree?二、k-d树的结构三、k-d树的创建四、k-d树的应用五、KD-Tree的优缺点 例题JZPFAR 一、什么是KD-Tree? KD-Tree,又称(k-dimensional tree),是一种基于二叉树的数据结构。它可以…

大项目推进

拉取最新的小组分支,创建自己开发分支(对应实现人) 任务分支(小组名-功能名-执行人,如:f1-login-zhangfei) 根据业务流程整理文档梳理与书写 定义领域模型(包括:query、…

通用医学人工智能基础模型(GMAI)

最近,Eric J. Topol和 Pranav Rajpurkar研究团队提出了一个通用医学人工智能基础模型,文章名字《Foundation models for generalist medical artificial intelligence》 模型研究进展包括:多模态架构,和自监督学习技术&#xff0…

知识库AI机器人客服接口对接-唯一客服系统文档中心

如果你的需求仅仅是对接自训练的ChatGPT接口,实现自己的个性化机器人,那么可以看看下面的个性化ChatGPT调用接口前提条件是已经搭建好了知识库服务,该服务默认监听端口8083 chat接口地址 POST http://127.0.0.1:8083/data_collection/searchS…

“伙伴+华为”体系,数字时代的新航标

如果从1994年中国实行税制改革,要求以“以计算机网络为依托”开展企业税务工作算起,转瞬间,中国企业的信息化、数字化建设已经走过了近三十年历程。 这期间,信息化、数字化成为了企业走向管理现代化、全球化的依托,成为…

最新版本的Android studio 集成高德地图的定位功能

android studio版本: 1、根据高德官网链接集成 2、配置key的时候有两个注意点: a .获取安全SHA1 根据高德推荐的方式获取时,可能C:\Program Files\Android\Android Studio\jre\bin目录下找不到keytool.exe; 可以根据以下方式获取&#xff1…

华为新模拟器eNSPLite下载,部署教程及产品使用文档

华为新模拟器eNSPLite下载,部署教程及产品使用文档 如需下载请到我的博客中下载 硬件要求 数通培训认证模拟器支持在个人PC和物理服务器上部署安装,如下所示。 硬件推荐配置CPUX86_64架构CPU,支持VT-x/AMD-V 8核或以上RAM16G或以上DISK40G以…

数据库信息速递 支持机器学习的10个数据库 (译)

开头还是介绍一下群,如果感兴趣polardb ,mongodb ,mysql ,postgresql ,redis 等有问题,有需求都可以加群群内有各大数据库行业大咖,CTO,可以解决你的问题。加群请联系 liuaustin3 ,在新加的朋友会分到2群(共…

Spring:自动装配 Bean 的两种方式、使用注解开发

文章目录 Spring:Day 02一、自动装配 Bean1. 搭建环境2. 自动装配方式一:xml 配置3. 自动装配方式二:注解 二、注解开发三、使用 JavaConfig 实现配置 Spring Spring:Day 02 一、自动装配 Bean 在 Spring 中有三种装配的方式&am…

PostgreSQL表用户列最大个数

PostgreSQL表用户列最大个数 有些业务可能有这么个需求:需要增加用户列,即通过ALTER TABLE ... ADD...来添加用户列。那么PG/GP中是否会有列个数的限制呢? 它有1600列数的限制,并且没有方法去除掉这个限制。参见: http…

千年平阴玫瑰,绽放数字新魅力

“人间美景五月天、玫瑰花放霞流丹。” 每年的五月,济南市平阴县总是一幅玫瑰花芳香如海的迷人景象。作为中国玫瑰之乡,平阴地处古东原之阴,位于北纬36度“玫瑰种植黄金带”之上,这里土肥地沃、气候温和,属暖温带大陆…

Netty实战(四)

本节我们看看Netty的传输(全是干货,自带水杯) 一、Java的NIO和OIO1.1 OIO1.2 NIO 二、Netty的NIO和OIO2.1 OIO2.2 NIO 三、传输API四、内置的传输4.1 NIO4.2 Epoll—用于 Linux 的本地非阻塞传输4.3 OIO4.4 用于 JVM 内部通信的 Local 传输4.…

chatgpt赋能Python-pycharm如何关联python

PyCharm如何关联Python 作为一款被广泛使用的Python集成开发环境(IDE),PyCharm为Python程序员提供了丰富的开发工具和功能。在开始使用PyCharm之前,我们需要确保PyCharm已经正确地关联了Python。在本篇文章中,我们将介…

chatgpt赋能Python-pycharm访问网页

PyCharm访问网页的SEO技巧 PyCharm是一款强大且广受欢迎的Python开发环境,它提供了许多优秀的工具和功能,以便于Python项目的开发和管理。其中一个强大的功能是PyCharm可以访问网页,可以使你快速获取和分析数据。但是,在使用这个…

POSTGRESQL 通过TRIGGER 解决数据库表丢失数据的问题

开头还是介绍一下群,如果感兴趣polardb ,mongodb ,mysql ,postgresql ,redis 等有问题,有需求都可以加群群内有各大数据库行业大咖,CTO,可以解决你的问题。加群请联系 liuaustin3 ,在新加的朋友会分到2群(共…

Consul系列:什么是Consul?

引言 Consul 是 HashiCorp 公司推出的开源工具,用于实现分布式系统的服务发现与配置。与其他分布式服务注册与发现的方案, consu1 的方案更“一站式”,内置了服务注册 与发现框架、分布一致性协议实现、健康检查、Key/Value 存储、多数据中心…