【JavaEE】wait/notify方法 和 单例模型

news2025/1/19 23:05:44

目录

前言

1、 wait和notify

1.1、wait()方法

1.2、notify()方法 

1.3、wait和sleep 的对比

2、单例模式 

2.1、饿汉模式

2.2、懒汉模式

2.3、上述懒汉模式和饿汉模式在多线程情况下是否安全

 2.3.1、解决懒汉模式多线程不安去问题


前言

  • 这里补充一下上一个博客中的volatile和内存可见性的知识点。网上有些资料在说内存可见性的时候,会说t1线程在频繁读取主内存,效率比较低,就被优化成直接读自己的工作内存,t2修改了主内存的结果,由于t1没有读主内存,导致修改不能被识别到。上述中的工作内存就相当于我们说的CPU寄存器,主内存就是内存。
  • 主内存和内存这一说法来自于jvm规范文档(官方说法),为什么要这么说,而不使用我们采用的说法,原因在于Java的跨平台性使Java能够1.兼容多种操作系统,2.兼容多种硬件设备,尤其是CPU。不同的硬件设备之间的差别很大,就以CPU为了,以前的CPU上面只有寄存器,而现在的CPU上不仅有寄存器还有缓存(现在常见的CPU都是3级缓存 L1,L2,L3).
  • 所以工作内存准确来说,表示的就是CPU寄存器+缓存(CPU内部存储数据的空间)
  • 了解一下缓存的知识点:
  1. 缓存有三个( L1,L2,L3),L1最小,但是L1读数据的速度最快(比寄存器慢);L3最大,但是读数据的速度最慢(比内存快很多)
  2. 缓存读取数据的速度介于寄存器和内存之间。(寄存器>缓存>内存>硬盘)。
  • 实际上CPU尝试读一个内存数据的步骤为:
  1. 先看看寄存器里有没有
  2. 寄存器中没有,再看L1里有没有,
  3. L1中没有,再看L2中有没有
  4. L2中没有,再看L3中有没有
  5. L3中没有,再看内存中有没有。

这样做的目的就是为了让程序更快,上述虽然读了很多次,但是寄存器读取数据的速率是内存的几千倍,几万倍,缓存也是同样的道理,所以比起读内存还是快很多。

1、 wait和notify

方法说明
wait()就是让某个线程暂停下来,等一等
wait(timeout)参数表示等待最长时间,到这个时间了就会自动唤醒
notify()唤醒等待的线程
notifyAll()唤醒等待同一个对象的多个线程

❗❗❗注意:wait、notify和notifyAll都是Object类的方法。只要是一个类对象(不是内置类型),都可以使用wait/notify.

由于线程的调度是无序的,随机的,但是有些场景需要线程是有序执行的,我们的wait和notify就是控制线程执行顺序的一种方式,就像之前的join也是一种控制线程执行顺序的一种方式。

1.1、wait()方法

wait主要做三件事

  1. 使当前执行代码的线程阻塞等待(把线程放到等待队列中)
  2. 释放当前的锁
  3. 满足一定条件是被唤醒,重新尝试获取锁。

❗❗❗注意wait要搭配synchronized来使用,脱离了synchronized使用wait会直接抛出异常。

1️⃣代码没有加锁,直接使用wait方法,代码抛异常 

public class ThreadDemo15 {
    public static void main(String[] args) throws InterruptedException {
        Object object = new Object();
        System.out.println("wait 之前");
        object.wait();
        System.out.println("wait 之后");
    }
}

上述代码就会抛出非法的锁状态异常 

 2️⃣正确的写法就是wait搭配synchronized使用。

public class ThreadDemo15 {
    public static void main(String[] args) throws InterruptedException {
        Object object = new Object();
        System.out.println("wait 之前");
        synchronized(object){//加锁的锁对象必须和调用wait的对象是同一个。
            object.wait();
        }
        System.out.println("wait 之后");
    }
}

❗❗❗注意:使用synchronized(加锁)的时候锁对象必须和调用wait的对象是同一个,notify也是同理,要放在synchronized中

❗❗❗wait结束等待的条件:

  1. 其他线程调用该对象的notify方法
  2. wait等待时间超时(wait方法提供一个带有timeout参数的版本,来指定等待时间)
  3. 其他线程调用该等待线程的interrupted方法,导致wait抛出 interruptedException异常。 

1.2、notify()方法 

notify方法是唤醒等待的线程

如果有多个线程等待同一个对象,notify会随机唤醒其中一个线程。

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

        });
        t1.start();

        Thread.sleep(1000);//表示的意思是让t1线程先执行,主线程休眠1s.

        Thread t2 = new Thread(()->{
            synchronized (locker){
                System.out.println("notify 开始");
                locker.notify();
                System.out.println("notify 结束");
            }
        });
        t2.start();
    }
}

 可以看到t1先执行,执行到wait了,就阻塞了,1s之后t2开始执行,执行到notify就会通知t1线程唤醒。(notify是在synchronized内部的,就需要t2线程释放了锁,t1才能继续往下执行。)所以wait在被唤醒的时候,会存在阻塞,需要等待t2线程将所释放了,t1才会被唤醒。

 在上述的代码中,虽然t1是先执行的,但是可以通过wait notify控制t2先执行一些逻辑。t2执行完之后,notify唤醒t1,t1在继续向下执行。

❗❗❗总结

1️⃣使用wait,阻塞等待会让线程进入Waiting状态 

2️⃣唤醒操作,还有一个 notifyAll, 可以有多个线程,等待同一个对象

比如在 t1 t2 t3 中都调用 object.wait ,此时在 main 中调用了 object.notify ——————会随机唤醒上述的一个线程(另外两个仍然是 waiting 状态)

如果是调用了 object.notifyAll, 此时就会把上述三个线程都唤醒,此时这三个线程就会重新竞争锁,然后依次执行
 

1.3、wait和sleep 的对比

wait有一个带参数的版本,用来体现超市时间,这个时候感觉好像和sleep差不多

1️⃣相同点:都可以提前唤醒(wait可以通过notify提前唤醒,sleep可以通过interrupt提前唤醒)

2️⃣最大的区别是初心不同:wait解决的是线程之间的顺序控制 ,sleep单纯是让当前线程休眠一会

3️⃣使用上存在明显的区别:wait要搭配锁使用,sleep不需要


2、单例模式 

单例模型是校招中最常考的设计模型之一。(1、单例模式,2、工厂模式)

单例模式是指某个类,在进程中只有唯一的一个实例。

设计模型

设计模型就好比象棋中的"棋谱"。马走日字,象走田字,这只是单纯的知道每个棋子该怎样走,而棋谱就是用来教会我们象棋的一些套路,这些套路不能让我们无敌,但是可以让我们将象棋玩的不是很差,而我们这里说到的设计模型和棋谱同理。

单例:表示单个实例,也就是单个对象。表示一个程序中,某个类只能创建出唯一一个实例(对象),不能创建多个对象。

Java中的单例模式,借助Java语法,保证某个类,只能够创建出一个实例,而不能new多次

✨在Java语法中,实现单例模式有很多种写法,我们这里了解两种写法

  1. 饿汉模式(体现的是急迫)
  2. 懒汉模式(体现的是从容):效率更高

🎇我们通过这样一个例子来了解懒汉和饿汉:

打开一个硬盘上的文件,读取文件内容,并显示出来。

  • 饿汉:把文件所有内容都读到内存中,并显示。
  • 懒汉:只把文件读一小部分,把当前屏幕填充上,如果用户翻页了,再读其他文件内容,如果不翻页,就省下了。

假设文件非常大,饿汉模式将文件一次性从硬盘读到内存中,文件打开可能要卡好长时间,但是懒汉模式就可以快速打开,一页的文件占用的内存很小。

2.1、饿汉模式

饿汉模式:在创建类的时候,直接通过new创建出来。

//把这个类设置为单例模式
class Singleton{
    //唯一实例的本体
    private static Singleton instance = new Singleton();
//被static修饰,该属性是类的属性(被static修饰,属于类对象),JVM中,每个类的类对象只有唯一一
//份,类对象里的这个成员自然也就是唯一一份了。
    //获取到实例的方法
    public static Singleton getInstance(){
        return instance;
    }
    private Singleton(){

    }
}

public class ThreadDemo17 {
    public static void main(String[] args) {
        //此时s1和s2是同一个对象
        Singleton s1 = Singleton.getInstance();
        Singleton s2 = Singleton.getInstance();
        //机制外部new实例,这时候new的这个对象是不成功的,会编译报错。
        Singleton s3 = new Singleton();
    }
}

饿汉方式创建单例模式,确实可以实现,但是饿汉模式在创建唯一实例的本体时机太早,只要类一加载就会常见这个实例,但是如果后面我们没有用到这个实例,就会浪费。

2.2、懒汉模式

我们更多的会使用懒汉模式来写单例模式

懒汉模式:核心思想,非必要,不创建。什么时候需要什么时候创建实例。

class SingletonLazy{
    private static SingletonLazy instance;
    public static SingletonLazy getInstance(){
        if(instance == null){
            instance = new SingletonLazy();
        }
        return instance;
    }
    private SingletonLazy(){ }
}
public class ThreadDemo18 {
    public static void main(String[] args) {
       SingletonLazy s1 = SingletonLazy.getInstance();
       SingletonLazy s2 = SingletonLazy.getInstance();
        System.out.println(s1 == s2);
    }

}

2.3、上述懒汉模式和饿汉模式在多线程情况下是否安全

根据多线程不安去的原因去分析:

  1. 多线程的抢占式执行,多线程之间的调度充满随机性。
  2. 多个线程同时修改同一个变量
  3. 针对变量的操作不是原子的
  4. 内存可见性问题
  5. 指令重排序问题

1️⃣饿汉模式

Thread在主方法中创建实例多线程t1,t2,t3都调用getInstance()方法,他们在并发执行中是否会产生bug?


答案是不会,因为在饿汉模式中getIntance()只存在读取操作,并没有修改的操作。所以饿汉模式在多线程中是线程安全的。

2️⃣懒汉模式

懒汉模式是多线程不安全的。

因为在懒汉模式的代码中getInstance()方法中即存在判断也存在修改操作。

存在的安全性问题一: 

存在这个执行过程会导致创建多个SingletonLazy对象,产生上述问题的原因就是没有保证两条语句的原子性。 我们可以通过使用synchronized代码块来包裹这两个语句。

    public static SingletonLazy getInstance(){
        synchronized (SingletonLazy.class){
            if(instance == null){
                instance = new SingletonLazy();
            }
        }
        return instance;
    }

众所周知加锁会减缓我们程序的运行速度,所以非必要不加锁。加锁之后的代码还存在一点问题,我们在第一次创建SingletonLazy对象时,需要加锁,但是在我们将对象创建好之后,就不需要再加锁了,可以直接读取对象的引用即可。所以在getInstance()方法中还需要再加入一个if判断。

    public static SingletonLazy getInstance(){
        //这个条件,判定是否要加锁,如果对象已经有了,就不必加锁了,此时直接返回instance
        if(instance == null){
            synchronized (SingletonLazy.class){
                if(instance == null){
                    instance = new SingletonLazy();
                }
            }
        }
        return instance;
    }

❓❓❓看到上述代码,有的老铁经过分析,认为两个if判断条件一摸一样,为什么要这样写?将内层的if去掉好像代码也能成立?


❗❗❗可定时不能去掉内层的if的。

  1. 第一个原因在于,这两个if的作用不一样,设计最外层的if是用来判断是否需要加锁,内层的if是用来判断是否需要创建实例。
  2. 第二个原因在于,这两个代码的执行时机相差很大,第二个if在锁内,多线程中如果一个线程将外层的if执行完了,但是走到synchronized代码块位置,由于锁竞争,别的线程在执行锁内程序,当前线程就需要阻塞等待,等到当前的线程拿到锁,并执行锁内的代码的时候,其他线程可能已经将SingletonLazy对象已经创建好了,这个时候通过内层的if进行判断,这样就保证不会创建多的SingletonLazy对象。

存在的第二个安全性问题: 

getInstance()方法中的instance= new SingletonLazy();会触发指令重排序。

这个操作可以分为三步:

  1. 创建内存(买房子)
  2. 调用构造方法/初始化(装修)
  3. 把内存地址,赋给引用(拿到钥匙)

我们这里2和3的操作顺序是可以颠倒的,先装修在拿钥匙,你拿到的是精装修房。先拿钥匙,在装修你拿到的是毛坯房。

现在有两个线程t1和t2,t1线程执顺序为1,3,2。t1线程刚好将3执行完,还没有执行2(初始化对象),此时t2线程执行了外层的if判断,判断结果为instance不为空,直接拿到了instance引用。t2线程拿到的对象就相当于是一个毛坯房,然后再调用这个对象中的方法进行操作时,产生的结果就会越来越离谱了。

针对第二个问题我们可以对私有变量instance加一个volatile关键字,禁止他进行指令重排序。

    volatile private static SingletonLazy instance;

 2.3.1、解决懒汉模式多线程不安去问题

这里只展示一下代码,存在的安全问题上面已经解决完了。

package threading;
class SingletonLazy{
    volatile private static SingletonLazy instance;
    public static SingletonLazy getInstance(){
        //这个条件,判定是否要加锁,如果对象已经有了,就不必加锁了,此时直接返回instance
        if(instance == null){
            synchronized (SingletonLazy.class){
                if(instance == null){
                    instance = new SingletonLazy();
                }
            }
        }
        return instance;
    }
    private SingletonLazy(){ }
}

❗❗❗总结:

单例模型,多线程安全问题。

  • 饿汉模型:天然就是安全的,getInstance中只是读操作
  • 懒汉模型:不安全,getInstance中不仅存在读操作,还有写操作。

解决懒汉模式的线程不安全的做法,分三步:

  1. 加锁,把if和new变成原子操作
  2. 双重if,减少不必要的加锁操作
  3. 使用volatile禁止指令重排序,保证后续线程肯定拿到完整的对象。

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

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

相关文章

网络安全面试题合集

以下为网络安全各个方向涉及的面试题,星数越多代表问题出现的几率越大,祝各位都能找到满意的工作。 注:本套面试题,已整理成pdf文档,但内容还在持续更新中,因为无论如何都不可能覆盖所有的面试问题&#xf…

瑞吉外卖 - 启用与禁用员工账号功能(8)

某马瑞吉外卖单体架构项目完整开发文档,基于 Spring Boot 2.7.11 JDK 11。预计 5 月 20 日前更新完成,有需要的胖友记得一键三连,关注主页 “瑞吉外卖” 专栏获取最新文章。 相关资料:https://pan.baidu.com/s/1rO1Vytcp67mcw-PD…

最新入河排污口设置论证、水质影响预测与模拟、污水处理工艺分析及典型建设项目入河排污口方案报告书

随着水资源开发利用量不断增大,全国废污水排放量与日俱增,部分河段已远远超出水域纳污能力。近年来,部分沿岸入河排污口设置不合理,超标排污、未经同意私设排污口等问题逐步显现,已威胁到供水安全、水环境安全和水生态安全&#x…

Packet Tracer – 配置 VLAN

Packet Tracer – 配置 VLAN 地址分配表 设备 接口 IP 地址 子网掩码 VLAN PC1 NIC 172.17.10.21 255.255.255.0 10 PC2 NIC 172.17.20.22 255.255.255.0 20 PC3 NIC 172.17.30.23 255.255.255.0 30 PC4 NIC 172.17.10.24 255.255.255.0 10 PC5 NI…

open3d 表面重建

目录 1. create_from_point_cloud_ball_pivoting 2. create_from_point_cloud_alpha_shape 3. create_from_point_cloud_poisson 从以下效果来看,第三个方法最好。 1. create_from_point_cloud_ball_pivoting 关键代码: rec_mesh o3d.geometry.T…

面试被问麻了....

前几天组了一个软件测试面试的群,没想到效果直接拉满,看来大家对面试这块的需求还是挺迫切的。昨天我就看到群友们发的一些面经,感觉非常有参考价值,于是我就问他还有没有。 结果他给我整理了一份非常硬核的面筋,打开…

2022年美国大学生数学建模竞赛F题人人为我,我为人人解题全过程文档及程序

2022年美国大学生数学建模竞赛 F题 人人为我,我为人人 原题再现: 背景:   世界上大多数国家签署了1967年联合国《外层空间条约》,条约内容包括同意探索和利用外层空间,包括月球和其他天体,不论各国经济或科学发展程…

低成本挖出电商API接口-程序员要注意那些事项-技术分享

在开发电商应用的过程中,获取天猫API接口是非常必要的一步。天猫API提供了丰富的商品数据获取、订单管理、支付管理等功能,但是天猫API一般需要进行开发者认证,而认证需要企业资质和若干费用支出,这对个人开发者和小型业务开发者来…

Postgresql数组与Oracle嵌套表的使用区别

oracle中的多维数组 Oracle中常说的数组就是嵌套表,下面给出两个多维使用实例,引出和PG的差异: 一维赋值(第一行给1列) set serveroutput on; declaretype arr_num is table of number;type arr_arr_num is table o…

任务队列的Java实现

一、需求背景 当前项目中遇到这样一个需求: 将需要审核的文本提交给人工智能模型接口审核,等待模型接口审核完毕以后拿到审核结果返回给前端展示给用户(另:模型处理数据所消耗的时间会随着用户提交数据的复杂度有所变化)。 以上需…

毫米波雷达系列 | 传统CFAR检测(自适应类)

毫米波雷达系列 | 传统CFAR检测(自适应类) VI-CFAR [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dV34CKJt-1684215839850)(毫米波雷达系列 传统CFAR检测(自适应类).assets/image-20230516131206695…

Recognizing Micro-Expression in Video Clip with Adaptive Key-Frame Mining阅读笔记

本文主要贡献 据我们所知,这是第一项旨在将视频剪辑中的信息时间子集的端到端学习与单个网络中的微表情识别相结合的工作。 此外,所提出网络中所有模块的设计都与输入视频剪辑的长度无关。 换句话说,网络容忍各种长度的微表情剪辑。 本文的贡…

水利工程电子(数字)沙盘

水利工程电子(数字)沙盘利用三维地理信息系统、遥感技术、海量数据管理技术、虚拟现实技术、网络通讯技术和高性能计算机技术等现代高新信息技术,采用高精度DEM地形数据、遥感影像、航拍影像和基础地理矢量数据,建立三维空间场景&…

SAAS 与 IAAS 云渲染农场比较

渲染时,最重要的是需要的时间和硬件可用的。此渲染过程需要大量计算能力才能快速创建图像或视频。GPU(图形处理单元)是图形的计算能力,越来越多地用于渲染CAD模型。GPU中有特殊的处理器,可以执行用于快速编辑和显示图像…

TCP的拥塞控制

为了避免发送方无节制地发送数据,从而造成网络拥堵,所以 TCP 有一个拥塞控制。 流量控制:作用于接收方,控制发送者发送速度,从而使接收者来得及接收,防止分组丢失。 拥塞控制:作用于网络&#…

【星戈瑞】Sulfo-Cyanine5 mal红色荧光Cy5-maleimide

Sulfo-Cyanine5 mal是一种具有强荧光信号的染料,主要应用于生物荧光成像领域。它的化学式为C38H43KN4O9S2,分子量为803.00。这种染料具有良好的水溶性,可在水溶液中稳定存在。它的光学特性包括吸收峰位于646 nm和发射峰位于662 nm&#xff0c…

公司新来了个00后软件测试工程师,一副毛头小子的样儿,哪想到是新一代的卷王...

内卷,是现在热度非常高的一个词汇,随着热度不断攀升,隐隐到了“万物皆可卷”的程度。 在程序员职场上,什么样的人最让人反感呢? 是技术不好的人吗?并不是。技术不好的同事,我们可以帮他。 是技术太强的人吗?也不是…

计算机组成简答题整理

作者:爱塔居 多模块交叉存储器是如何加速CPU与存储器之间有效传输? 解:CPU同时访问多个模块,由存储器控制部件控制它们分别使用数据总线进行信息传递。对每一个存储模块来说,从CPU给出访存命令直到读出信息仍然使用了…

同惠 LCR数字电桥测试仪 TH2830

TH283X系列是新一代低成本,高性能紧凑型LCR数字电桥,采用最新工艺和高密度电路设计,浓缩大型LCR测试仪的精华,紧凑,小巧.取消传统机械电源开关,采用软件控制电源开关.0.05%的基本精度和良好的测试稳定性可与高端机型媲美.配备4.3寸LCD显示屏和全新升级的界面系统.美观大方,操作…

EXCEL: 查找符合多个条件,并且不重复的数据的个数的3种方法:公式,数据透视表,数组公式

1 目标问题:想筛选出(在a列月份为5)且不重复的b列数据有几个 有2个条件 查找第1列月份为5月的并且第2列不重复的数据个数 方法1:用加辅助列简单公式的办法 其实逻辑是更清晰的,就是显得步骤繁琐 第1个辅助列1,查找日期中的月份…