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

做 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.mfbz.cn/a/346157.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Vue实现图片预览,侧边栏懒加载,不用任何插件,简单好用

实现样式 需求 实现PDF上传预览&#xff0c;并且不能下载 第一次实现&#xff1a;用vue-pdf&#xff0c;将上传的文件用base64传给前端展示 问题&#xff1a; 水印第一次加载有后面又没有了。当上传大的pdf文件后&#xff0c;前端获取和渲染又长又慢&#xff0c;甚至不能用 修…

2. figure 常见属性

2. figure 常见属性 一 figsize二 dpi三 facecolor四 edgecolor五 frameon 数据可视化是数据分析中不可或缺的一环&#xff0c;而Matplotlib作为Python中最流行的绘图库之一&#xff0c;扮演着重要的角色。在Matplotlib中&#xff0c;matplotlib.figure.Figure对象是构建图形的…

MyBatis详解(2)-- mybatis配置文件

MyBatis详解&#xff08;2&#xff09; mybatis配置文件 mybatis配置文件 1.构建SqlSessionFactory的依据。 2.MyBatis最为核心的内容&#xff0c;对MyBatis的使用影响很大。 3.配置文件的层次顺序不能颠倒&#xff0c;一旦颠倒会出现异常。 < c o n f i g u r a t i o n…

openresty 安装, nginx与 openresty

openresty VS nginx Nginx 是一款高性能的 Web 服务器和反向代理服务器&#xff0c;具备基础的功能如HTTP服务、负载均衡、反向代理以及动静分离等。它是许多互联网应用的核心组件&#xff0c;因其模块化和可扩展的设计而受到欢迎。1 OpenResty 是基于 Nginx 的 Web 平台&…

C++入门篇章1(C++是如何解决C语言不能解决的问题的)

目录 1.C关键字(以C98为例)2.命名空间2.1 命名空间定义2.2命名空间使用 3.C输入&输出4.缺省参数4.1缺省参数概念4.2 缺省参数分类 5. 函数重载5.1函数重载概念5.2 C支持函数重载的原理--名字修饰(name Mangling) 1.C关键字(以C98为例) C总计63个关键字&#xff0c;C语言32…

【操作系统基础】【CPU访存原理】:寄存 缓存 内存 外存、内存空间分区、虚拟地址转换、虚拟地址的映射

存储器怎么存储数据、内存空间分区、虚拟地址转换 计算机的存储器&#xff1a;寄存 缓存 内存 外存&#xff08;按功能划分&#xff09; 计算机的处理器需要一个存储器来存储大量的指令和数据以便自己不断取指执行和访问数据。 内存&#xff08;内存就是运行内存&#xff0c…

利用git上传本地文件

1、建立仓库 2.然后刷新网站&#xff0c;获取下载链接&#xff0c;备用。 3、接下来在本地创建一个文件夹&#xff0c; 4、把github上面的仓库克隆到本地 git clone https://github.com/xxxxx&#xff08;https://github.com/xxxxx替换成你之前复制的地址&#xff09; 5、把…

简单快速取消AlertDialog的白色背景框,AlertDialog设置圆角背景

问题描述&#xff1a; 产品需求弹出的提示框是圆角&#xff0c;使用shape 设置圆角背景后&#xff0c;弹出的AlertDialog提示框四个角有白色的背景&#xff0c;据分析这个背景是 AlertDialog 父组件的背景色。 解决方法&#xff1a; 将Dialog的背景设置为透明色&#xff0c;代…

生产力工具|卸载并重装Anaconda3

一、Anaconda3卸载 &#xff08;一&#xff09;官方方案一&#xff08;Uninstall-Anaconda3-不能删除配置文件&#xff09; 官方推荐的方案是两种&#xff0c;一种是直接在Anaconda的安装路径下&#xff0c;双击&#xff1a; &#xff08;可以在搜索栏或者使用everything里面搜…

两数之和[中等]

一、题目 给你一个下标从1开始的整数数组numbers&#xff0c;该数组已按非递减顺序排列&#xff0c;请你从数组中找出满足相加之和等于目标数target的两个数。如果设这两个数分别是numbers[index1]和numbers[index2]&#xff0c;则1 < index1 < index2 < numbers.len…

Oracle报错:ORA-12541:TNS:无监听程序 (很大概率是listener.log满了,4G就无法写入了)

目录标题 一、前提二、查看listener.log三、如果是listener.log满了&#xff0c;内存达到4G,可以使用以下方法解决。&#xff08;一&#xff09;停用服务&#xff08;二&#xff09;将满了的listener.log日志删除或者改名&#xff0c;然后新建一个一样的listener.log文件&#…

助力工业生产质检,基于YOLOv7【tiny/l/x】不同系列参数模型开发构建生产制造场景下布匹瑕疵缺陷检测识别分析系统

纯粹的工业制造没有办法有长久的发展过程&#xff0c;转制造为全流程全场景的生产智造才是未来最具竞争力的生产场景&#xff0c;在前面的开发实践中我们已经涉足工业生产场景下进行了很多实地的项目开发&#xff0c;如&#xff1a;PCB电路板缺陷检测、焊接缺陷检测、螺母螺钉缺…

面试题-【消息队列】

消息队列 问题1 如何进行消息队列的技术选型优点解耦 &#xff08;pub/sub模型&#xff09;异步&#xff08;异步接口性能优化&#xff09;削峰 使用消息队列的缺点几种消息队列的特性 问题2 引入消息队列之后该如何保证其高可用性RabbitMQ的高可用kafka高可用 问题3 在消息队列…

07 队列

目录 1.队列 2.实现 3.OJ题 1. 队列 只允许在一段进行插入数据操作&#xff0c;在另一端进行数据删除操作的特殊线性表&#xff0c;队列具有先进先出FIFO&#xff08;First In Firtst Out&#xff09;&#xff0c;插入操作的叫队尾&#xff0c;删除操作的叫队头 2. 实现 队列…

前端echarts图形报表常见的样式配置

文章目录 &#x1f412;个人主页&#x1f3c5;Vue项目常用组件模板仓库&#x1f4d6;前言&#xff1a;&#x1f415;1.深色主题&#x1f415;2.改变柱状图颜色&#x1f415;突然发现去问ai&#xff0c;更容易理解&#xff0c;那就不总结了 &#x1f412;个人主页 &#x1f3c5;…

太阳光模拟器汽车耐老化太阳跟踪聚光户外加速老化试验

1 范围 1.1 本标准适用于以太阳为光源的菲涅耳反射系统来进行汽车外饰材料的加速老化试验。 1.2 本标准规定的设备和方法可用于确定曝露于日光、热和潮湿下的汽车材料的相对耐老化性&#xff0c; 前提是假设试验期间发生的对材料加速老化速率起决定性作用的物理、化学变化机理…

缓存高并发问题

Redis 做缓存虽减轻了 DBMS 的压力&#xff0c;减小了 RT&#xff0c;但在高并发情况下也是可能会出现各种问题的。 缓存穿透 当用户访问的数据既不在缓存也不在数据库中时&#xff0c;就会导致每个用户查询都会“穿透”缓存“直抵”数据库。这种情况就称为缓存穿透。当高度发…

什么是网络?

你是一台电脑&#xff0c;你的名字叫 A 很久很久之前&#xff0c;你不与任何其他电脑相连接&#xff0c;孤苦伶仃。 直到有一天&#xff0c;你希望与另一台电脑 B 建立通信&#xff0c;于是你们各开了一个网口&#xff0c;用一根网线连接了起来。 用一根网线连接起来怎么就能&…

Oracle BIEE 示例(一)数据透视表2

1 背景 版本:BIEE 12C 视图:数据透视表 实现内容(顺序与具体内容不一致): 2 空列显示(方法一) 2.1 问题 列为空时,标题栏不显示信息。 2.2 期望 即使数据为空,也要显示列名。 2.3 官方资料 2.3.1 操作步骤 2.3.1.1 要在分析级别关闭空值隐藏,请执行以下操作…

不停机迁移,TDengine 在 3D 打印技术中的“焕新”之路

小T导读&#xff1a;自 2021 年我们正式使用 TDengine 至今已接近三年&#xff0c;现在 TDengine 已经成熟应用于我们多个项目当中&#xff0c;凭借着强大的读写存储能力&#xff0c;为我司多项业务的核心数据保驾护航。近期我们团队刚好完成 TDengine 2.x 到 3.x 的数据迁移&a…
最新文章