面试知识点:notify是随机唤醒线程吗(唤醒线程顺序)?

news2024/9/28 19:16:11

做 Java 开发的小伙伴,对 wait 方法和 notify 方法应该都比较熟悉,这两个方法在线程通讯中使用的频率非常高,但对于 notify 方法的唤醒顺序,有很多小伙伴的理解都是错误的,有很多人会认为 notify 是随机唤醒的,但它真的是随机唤醒的吗?

带着这个疑问,我们尝试休眠 100 个线程,再唤醒 100 个线程,并把线程休眠和唤醒的顺序保持到两个集合中,最后再打印一下这两个集合,看一下它们的执行顺序,如果它们的顺序是一致的,那说明 notify 是顺序唤醒的,否则则是随机唤醒的,notify 测试代码如下:

import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.TimeUnit;

public class NotifyTest {
    //等待列表, 用来记录等待的顺序
    private static List<String> waitList = new LinkedList<>();
    //唤醒列表, 用来唤醒的顺序
    private static List<String> notifyList = new LinkedList<>();

    private static Object lock = new Object();


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

        //创建50个线程
        for(int i=0;i<50;i++){
            String threadName = Integer.toString(i);
            new Thread(() -> {
                synchronized (lock) {
                    String cthreadName = Thread.currentThread().getName();
                    System.out.println("线程 ["+cthreadName+"] 正在等待.");
                    waitList.add(cthreadName);
                    try {
                        lock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("线程 ["+cthreadName+"] 被唤醒了.");
                    notifyList.add(cthreadName);
                }
            },threadName).start();

        }

        for(int i=0;i<50;i++){
            synchronized (lock) {
                lock.notify();
            }
            TimeUnit.MILLISECONDS.sleep(10);
        }
        System.out.println("wait顺序:"+waitList.toString());
        System.out.println("唤醒顺序:"+notifyList.toString());
    }
}

执行结果如下:

wait顺序:[0, 2, 3, 1, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 47, 46, 48, 49]
唤醒顺序:[0, 2, 3, 1, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 47, 46, 48, 49]

从上述打印的结果我们可以看出,使用 notify 并不是随机唤醒的,而是顺序唤醒的,虽然以上代码能证明这个结论,但为了更清楚的解释这个问题,我们查看了 notify 的实现源码,它的源码内容如下:

    /**
     * Wakes up a single thread that is waiting on this object's
     * monitor. If any threads are waiting on this object, one of them
     * is chosen to be awakened. The choice is arbitrary and occurs at
     * the discretion of the implementation. A thread waits on an object's
     * monitor by calling one of the {@code wait} methods.
     * <p>
     * The awakened thread will not be able to proceed until the current
     * thread relinquishes the lock on this object. The awakened thread will
     * compete in the usual manner with any other threads that might be
     * actively competing to synchronize on this object; for example, the
     * awakened thread enjoys no reliable privilege or disadvantage in being
     * the next thread to lock this object.
     * <p>
     * This method should only be called by a thread that is the owner
     * of this object's monitor. A thread becomes the owner of the
     * object's monitor in one of three ways:
     * <ul>
     * <li>By executing a synchronized instance method of that object.
     * <li>By executing the body of a {@code synchronized} statement
     *     that synchronizes on the object.
     * <li>For objects of type {@code Class,} by executing a
     *     synchronized static method of that class.
     * </ul>
     * <p>
     * Only one thread at a time can own an object's monitor.
     *
     * @throws  IllegalMonitorStateException  if the current thread is not
     *               the owner of this object's monitor.
     * @see        java.lang.Object#notifyAll()
     * @see        java.lang.Object#wait()
     */
    public final native void notify();

简单翻译一下上面的重点内容,notify 选择唤醒的线程是任意的,但具体的实现还要依赖于 JVM。也就是说 notify 的唤醒规则,最终取决于 JVM 厂商,不同的厂商的实现可能是不同的,比如阿里的 JVM 和 Oracle 的 JVM,关于 notify 的唤醒规则可能是不一样的。

那作为一个普通的程序员我们要研究的就是官方的 JVM 也就是 HotSpot 虚拟机,它的 notify 实现源码在 ObjectMonitor.cpp 中,具体源码如下:

DequeueWaiter 方法实现的源码如下:

 从上述源码可以看出,在进行唤醒时,每次会从 _WaitSet 等待集合中获取第一个元素进行出队操作,这也说明了 notify 是顺序唤醒的。

总结

notify 唤醒线程的规则是随机唤醒还是顺序唤醒取决于 JVM 的具体实现,作为主流的 HotSpot 虚拟机中的 notify 的唤醒规则是顺序的,也就是 notify 会按照线程的休眠顺序,依次唤醒线程。


重量级锁(Monitor)的加锁和解锁流程?解锁是有顺序的吗?

重量级锁 MonitorObject 对象有 4 个属性,分别是:

  • _owner:当前锁的持有线程

  • _cxq:竞争栈

  • _entryList:一个队列

  • _waitSet:

在 Monitor 内部中,主要有四部分组成,分别是 owner、cxq、EntryList 和 waitSet。

1、其中 owner 表示当前所的持有者,记录是哪一个线程获取了当前锁;

2、cxq 是一个栈结构,EntryList 是一个队列结构,这两部分一起完成了当发生锁竞争时,记录线程的阻塞状态;

3、waitSet 是一个集合结构,当线程执行 wait 方法后会将当前线程存入到 waitSet 集合中进入等待状态,只有当执行 notify 或者 notifyAll 时才会唤醒 waitSet 中的相关线程。

从 waitSet中唤醒的线程并不会马上获取锁,而是会和其他线程一样进行锁的竞争操作。

_entryList和_cxq是锁的等待队列,_waitSet是调用了wait()方法的线程队列

加锁流程:

当线程 t1、t2、t3 一起获取一个重量级锁时,获取的时间顺序分别是 t1、t2 和 t3。

1、因为是线程 t1 首先到达,所以 t1 会获取成功, MonitorObject 的 _owner 会从 nullptr 变成 t1,线程 t1 的 markword 对象存储 MonitorObject 的地址引用并将最后两位标记为 10,表示重量级锁。此时线程 t2 和 t3 肯定获取锁失败。

2、线程 t2,t3 开获取失败后悔开始进行自旋操作【jdk1.6 以后固定自旋就弃用了】,首先预自旋 11 次,获取锁失败之后会自适应自旋,首先自旋 5000 次。在自旋期间若线程 t1 释放锁了,此时线程 t2 和 t3 会一起去抢占锁,若没有释放就会进入 _cxq 竞争栈中。这段时间还是抢占式的。

3、若第二步获取锁失败了,就会进入一个叫做 enterI 的方法,尝试获取 _owner ,失败之后会陷入自旋,较上次自旋次数少 200 次,若自旋期间获取成功就成功拿到锁了,这段时间还是抢占式的。

4、若第 3 步中还是获取失败了那么线程就会在 _cxq 中陷入阻塞状态了(park),直到 _owner 被释放才会被唤醒。从这里开始就是非抢占式的了。靠后竞争锁的线程会优先获取到锁。

从加锁解锁流程可以看出,线程会先进入 cxq ,当 owner 释放后才会将 cxq 中的唤醒进入 EntryList 队列,然后再获取锁。

其实这么做的主要目的是为了防止出现 ABA 问题

相关视频:【Java必备知识】为锁正名-第9集-重量级锁阻塞队列为何分成cxq和EntryList_哔哩哔哩_bilibili

解锁流程:

当线程 t1 释放锁之后,就会将 _owner 设置成 nullptr。此时会根据 _cxq 和 _entryList 的状态做出不同的操作。

1、当_cxq 和 _entryList 都为空时直接返回,释放成功。

2、当 _cxq 不为空时,就会将 _cxq 中所有的节点移动到 _entryList 中,_cxq 按照后进先出的原则,之后进入 _cxq 的会先进入 _entryList。

3、当 _entryList 不为空时,使用 unpark 方法从队列头结点开始唤醒,然后返回。

所以说,根据上面的加锁流程,当 t1 释放锁之后,进入 _cxq 的顺序是先 t2 后 t3,所以离开 _cxq 进入 _entryList 的顺序是先 t3 后 t2。故在 t2 和 t3 中,t3 会先获得锁。

解锁是有序的验证

查看下列代码

 private static Object obj = new Object();
​
    public static void main(String[] args) throws InterruptedException {
​
        new Thread(() -> {
            System.out.println("t1 获取锁");
            synchronized (obj) {
                try {
                    System.in.read();
                    System.out.println("t1 释放");
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
​
        }).start();
​
        Thread.sleep(100);
        new Thread(() ->{
            synchronized (obj){
                System.out.println("t2 获取");
            }
        }).start();
​
        Thread.sleep(100);
        new Thread(() ->{
            synchronized (obj){
                System.out.println("t3 获取");
            }
        }).start();
​
        Thread.sleep(100);
        new Thread(() ->{
            synchronized (obj){
                System.out.println("t4 获取");
            }
        }).start();
​
    }

运行结果如下(运行多次结果都是一样的):从结果可以看出,当线程 t1 释放锁后,越靠后竞争锁的线程或优先抢占到锁。这就是上面加锁流程中第 4 步的体现。

t1 获取锁

t1 释放
t4 获取
t3 获取
t2 获取

Process finished with exit code 0


notify

  • notify()随机唤醒一个处在等待状态的线程  
  • notifyAll()唤醒所有处在等待状态的线程
  • 方法notify()也要在同步方法或同步块中调用,该方法是用来通知那些可能等待该对象的对象锁的 其它线程,对其发出通知notify,并使它们重新获取该对象的对象锁。
  • 如果有多个线程等待,则有线程调度器随机挑选出一个呈 wait 状态的线程。(并没有 "先来后到") 在notify()方法后,当前线程不会马上释放该对象锁,要等到执行notify()方法的线程将程序执行完,也就是退出同步代码块之后才会释放对象锁。

唤醒和阻塞具体的使用 

package thread.wait_notify;
 
public class waitDemo {
    private static class WaitTask implements Runnable{
        private Object lock;
        public WaitTask(Object lock){
            this.lock=lock;
        }
        @Override
        public void run() {
            synchronized (lock){
                System.out.println(Thread.currentThread().getName()+ "准备进入等待状态");
                try {
                    lock.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+ "等待结束,线程继续执行");
            }
        }
    }
    private static class Notify implements Runnable{
        private Object lock;
        public Notify(Object lock){
            this.lock=lock;
        }
        @Override
        public void run() {
            synchronized (lock){
                System.out.println("准备唤醒等待线程");
                //随机唤醒一个等待线程
                lock.notify();
                System.out.println("唤醒结束");
            }
        }
    }
    public static void main(String[] args) throws InterruptedException {
            Object lock=new Object();
            Thread t1=new Thread(new WaitTask(lock),"t1");
            Thread t2=new Thread(new WaitTask(lock),"t2");
            Thread t3=new Thread(new WaitTask(lock),"t3");
            Thread notify=new Thread(new Notify(lock),"notify");
            t1.start();
            t2.start();
            t3.start();
            Thread.sleep(100);
            notify.start();
    }
}

 

注意点

必须搭配synchroized使用,不然会直接报错 

 

背后工作原理解析

  • 如果是调用notifyAll,就会将这三个线程都放入阻塞队列,然后进行竞争锁资源
  • 一定要明确锁的资源是谁,引起的竞争的必须是线程调用的锁的对象一定是要一样的,如果竞争的不是同一个锁,那么就不会进入同一个阻塞队列
  • 只有唤醒线程执行完毕,才会有阻塞队列线程的执行
  • 阻塞队列怎么理解,比如当前t1线程获得了锁资源,那么t2,t3如果想竞争这个锁,就得处于阻塞队列,当t1线程调用了wait方法,释放了锁资源,那么t2和t3就会去竞争锁资源,然后其中获得一个,依次类推,当三个线程都处于等待队列,当调用了notify线程,等待队列其中一个进入阻塞队列,但是阻塞队列就算只有一个线程,也不会立即得到锁,因为notify线程也会占用锁,必须等notify线程结束,释放锁

wait和sleep的区别

  • 其实理论上 wait 和 sleep 完全是没有可比性的,因为一个是用于线程之间的通信的,一个是让线程阻塞一段时间, 唯一的相同点就是都可以让线程放弃执行一段时间
  • 如果有共性就先介绍共性,如果没有,分别介绍即可
  • wait方法是Object类提供的方法,需要搭配synchroized锁来使用,调用wait方法会释放锁,等待线程会被其他线程唤醒或者超时自动唤醒,唤醒之后需要再次竞争synchronized锁才能继续执行
  • sleep是Thread类提供的方法(不一定要搭配synchronized使用),调用sleep方法进入TIMED_WAITING状态,如果占用锁也不会释放锁,时间到了自动唤醒

为什么线程通信的方法 wait(), notify()和 notifyAll()被定义在 Object 类里

因为Java所有类的都继承了Object,Java想让任何对象都可以作为锁,并且 wait(),notify()等方法用于等待对象的锁或者唤醒线程,在 Java 的线程中并没有可供任何对象使用的锁,所以任意对象调用方法一定定义在Object类中。

为什么 wait(), notify()和 notifyAll()必须在同步方法或者同步块中被调

当一个线程需要调用对象的 wait()方法的时候,这个线程必须拥有该对象的锁,接着它就会释放这个对象锁并进入等待状态(等待队列)直到其他线程调用这个对象上的 notify()方法。同样的,当一个线程需要调用对象的 notify()方法时,它会释放这个对象的锁(在执行完锁的代码内容),以便其他在等待的线程就可以得到这个对象锁。由于所有的这些方法都需要线程持有对象的锁,这样就只能通过同步来实现,所以他们只能在同步方法或者同步块中被调用。

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

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

相关文章

力扣日记1.25-【回溯算法篇】39. 组合总和

力扣日记&#xff1a;【回溯算法篇】39. 组合总和 日期&#xff1a;2023.1.25 参考&#xff1a;代码随想录、力扣 39. 组合总和 题目描述 难度&#xff1a;中等 给你一个 无重复元素 的整数数组 candidates 和一个目标整数 target &#xff0c;找出 candidates 中可以使数字和…

完成接口配置实验

步骤一&#xff1a;IP地址规划 步骤二&#xff1a;配置二层设备 生产区内配置设备 办公区内配置设备 配置 DMZ区地址 valn配置 [Huawei]vlan batch 2 3 [Huawei]int g 0/0/2 [Huawei-GigabitEthernet0/0/2]port link-type access [Huawei-GigabitEthernet0/0/2]port…

【数据结构】链表的分类和双向链表

本篇是基于上篇单链表所作&#xff0c;推荐与上篇配合阅读&#xff0c;效果更加 http://t.csdnimg.cn/UhXEj 1.链表的分类 链表的结构非常多样&#xff0c;以下情况组合起来就有8种&#xff08;2 x 2 x 2&#xff09;链表结构&#xff1a; 我们一般叫这个头为哨兵位 我们上回…

LeetCode做题总结 226. 翻转二叉树

226. 翻转二叉树 代码1 报错代码2 报错代码3 正确。 代码1 报错 class Solution {public TreeNode invertTree(TreeNode root) {// TreeNode _root root; // 这是在保证// _root preOrderTree(root);// return root;root preOrderTree(root);return root;}public TreeNode …

MATLAB中uibutton函数用法

目录 语法 说明 示例 创建普通按钮 创建状态按钮 设置和访问按钮属性值 编写响应以下操作的代码&#xff1a;点击按钮 uibutton函数的功能是创建普通按钮或状态按钮组件。 语法 btn uibutton btn uibutton(parent) btn uibutton(style) btn uibutton(parent,style…

【方法论】费曼学习方法

费曼学习方法是由诺贝尔物理学奖得主理查德费曼提出的一种学习方法。这种方法强调通过将所学的知识以自己的方式解释给别人来提高学习效果。 费曼学习方法的步骤如下&#xff1a; 选择一个概念&#xff1a;选择一个要学习的概念或主题。 理解和学习&#xff1a;用自己的方式学…

全栈工程师

很多人说&#xff0c;全栈工程师是程序猿的终极目标。过去&#xff0c;笔者也一直往这个方面在努力&#xff0c;前后端代码都能写&#xff0c;测试也做一做&#xff0c;原型也画一画。但随着深耕it行业越来越久&#xff0c;笔者觉得条条大路通罗马&#xff0c;作为骡马的我们&a…

HCIA-HarmonyOS设备开发认证-1.HarmonyOS简介

目录 前言目标一、HarmonyOS简介1.1、初识HarmonyOS1.2、HarmonyOS典型应用场景 二、HarmonyOS架构与安全2.1、HarmonyOS架构2.1.1 内核层2.1.2 系统服务层2.1.3 框架层2.1.4 应用层 2.2、HarmonyOS安全 3、HarmonyOS关键特性4、HarmonyOS生态5、思考题坚持就有收获 前言 本章…

web安全学习笔记【08】——算法1

思维导图在最后 #知识点&#xff1a; 1、Web常规-系统&中间件&数据库&源码等 2、Web其他-前后端&软件&Docker&分配站等 3、Web拓展-CDN&WAF&OSS&反向&负载均衡等 ----------------------------------- 1、APP架构-封装&原生态&…

大语言模型推理提速:TensorRT-LLM 高性能推理实践

作者&#xff1a;顾静 TensorRT-LLM 如何提升 LLM 模型推理效率 大型语言模型&#xff08;Large language models,LLM&#xff09;是基于大量数据进行预训练的超大型深度学习模型。底层转换器是一组神经网络&#xff0c;这些神经网络由具有 self-attention 的编码器和解码器组…

Docker安装Clickhouse详细教程

简介 ClickHouse是一种列式数据库管理系统&#xff0c;专门用于高性能数据分析和数据仓库应用。它是一个开源的数据库系统&#xff0c;最初由俄罗斯搜索引擎公司Yandex开发&#xff0c;用于满足大规模数据分析和报告的需求。 特点 开源的列式存储数据库管理系统&#xff0c;…

好书推荐丨豆瓣评出9.2高分!Python编程入门就看蟒蛇书

目录 写在前面 内容简介 业内专家推荐 编辑推荐 资源丰富 作者介绍 Q&A 粉丝福利 写在后面 写在前面 在这日新月异的科技新时代&#xff0c;编程如同一把万能钥匙&#xff0c;为无数人打开了通向无限可能的大门。而在众多编程语言中&#xff0c;Python无疑是最耀…

工业阀门3D数字化展厅降低投入成本

大型机械设备的销售面临诸多挑战&#xff0c;其中最突出的问题之一是展示区域的限制。与家用小汽车不同&#xff0c;大型机械设备的展示需要大面积的场地&#xff0c;同时还需要展示各种型号和功能&#xff0c;这使得在一个城市内实现网格布点覆盖整座城市的难度加大。 为了解决…

DataStream API(转换算子)

目录 源算子 转换算子 1&#xff0c;基本转换算子 1.1映射&#xff08;map&#xff09; 1.2过滤&#xff08;filter&#xff09; 1.3扁平映射&#xff08;flatMap&#xff09; 2&#xff0c;聚合算子 2.1按键分区&#xff08;keyBy&#xff09; 2.2简单聚合 3&#x…

医学图像跨模态转换:配准方法生成对图 + 扩散模型 + 成对配对方法

医学图像跨模态转换&#xff1a;配准方法生成对图 扩散模型 成对配对方法 MRI 到 CT 的高精度转换X 光、核磁共振 MRI、CT 区别最关键的配准方法读后启发&#xff1a;Fundus 转 OCT &#xff08;只是猜想&#xff0c;不一定&#xff09; 数据图像预处理5 个图像转换算法评估图…

数据建模分析与数仓维度建模规范

前言&#xff1a;数据建模是将定义现实世界的数据抽象成模型的过程&#xff0c;以便更好的分析&#xff0c;管理和操作数据实现需求。数据建模在数仓管理、数据库设计、算法模型训练等领域都有着重要的作用。在不同的领域&#xff0c;数据建模的关注点不同&#xff1a;在数据库…

K8S搭建(centos)七、节点加入集群

天行健&#xff0c;君子以自强不息&#xff1b;地势坤&#xff0c;君子以厚德载物。 每个人都有惰性&#xff0c;但不断学习是好好生活的根本&#xff0c;共勉&#xff01; 文章均为学习整理笔记&#xff0c;分享记录为主&#xff0c;如有错误请指正&#xff0c;共同学习进步。…

53-JS之BOM,打开,关闭窗口,screen对象,history对象,location对象,工作区尺寸,滚动距离

1.简介 BOM(Browser Object Model)---浏览器对象模型,提供JS当中对浏览器的各种操作对象 1.1BOM与DOM 2.打开窗口window.open(URL,name,features) 2.1 URL字符串:地址网址文件源 2.2name:指target属性,规定在哪个窗口打开新的url链接 blank:打开一个新窗口 _parent…

[足式机器人]Part2 Dr. CAN学习笔记- 最优控制Optimal Control Ch07-4 轨迹追踪

本文仅供学习使用 本文参考&#xff1a; B站&#xff1a;DR_CAN Dr. CAN学习笔记 - 最优控制Optimal Control Ch07-4 轨迹追踪 1. 目标误差控制-误差的调节2. 稳态非零值控制3. 输入增量控制 1. 目标误差控制-误差的调节 2. 稳态非零值控制 3. 输入增量控制

Wpf 使用 Prism 实战开发Day14

备忘录接口增删&#xff08;CURD&#xff09;改查实现 一.添加备忘录控制器&#xff08;MemoController&#xff09; 备忘录控制器&#xff08;MemoController&#xff09;和待办事项控制器 &#xff08;ToDoController&#xff09;功能实现差不多一样。基本套路就是&#xff1…