当前位置: 首页 > news >正文

【操作系统】volatile、wait和notify以及“单例模式”基础知识

目录

1.volatile关键字:

2.wait和notify:

3.单例模式:


1.volatile关键字:

  1. 内存可见性问题是在编译器优化的背景下,一个线程把内存给改了,另外一个线程不能及时感知到。为了解决这种问题就引入了volatile。
  2. volatile的存在是为了保证内存可见性,但是并不保证原子性针对一个线程读,一个线程修改,这个场景volatile合适,而针对俩个线程修改,volatile做不到!
  3. 当使用volatile来修饰变量时,编译器就不会做出只读寄存器不读内存这样的优化这个关键字只能修饰变量,没有别的用法。
  4. volatile禁止了编译器优化,避免直接读取CPU寄存器中缓存的数据,而是每次都重新读内存。
  5. 面试中遇到volatile,多半也不会脱离JMM(Java Memory Model  java内存模型)工作内存不是真的内存,主内存才是真的内存。
  6. 站在JMM的角度来看待volatile;正常程序执行的过程中会把内存的数据先加载到工作内存中,再进行计算处理。编译器优化可能会导致不是每次都真的读取主内存,而是直接读取工作内存中的缓存数据(这就可能导致内存可见性问题)。而volatile起到的作用就是保证每次读取数据都是从工作内存上重新读取

2.wait和notify:

  1. 线程有个特别的地方,抢占式执行,线程调度的过程是随机的!而wait和notify是用来调配线程顺序的,让线程按照想要的顺序来调配,控制多线程之间的执行先后顺序。
  2. wait是Object的方法,线程执行到wait就会发生阻塞,直到另外一个线程调用notify把这个wait唤醒,才会继续往下走。wait只会影响调用的那个线程,不影响其他线程。
  3. wait操作本质上做了三件事:
    1.释放当前锁
    2.进行等待通知
    3.满足一定条件的时候(别人调用了notify),
         wait被唤醒然后尝试重新获取锁
  4. wait等待通知的前提是要把锁释放,而释放锁的前提是你得先加了锁。没锁怎么释放!因此wait的第一步操作就是先释放锁,保证其他线程能够正常往下运行,wait和加锁操作是密不可分的。
  5. 线程1没有释放锁的话,线程2就无法调用到notify;线程1调用了wait,在wait里面就自动释放锁了,这个时候虽然线程1阻塞在synchronized里面,但是此时锁是释放状态,线程2能拿到锁。其他线程必须要上锁才能调用notify,调用了notify才会唤醒wait,但是notify所在的线程也得先释放锁,wait才会在唤醒后的第一件事就是尝试重新加锁。
  6. 要保证加锁的对象和调用wait的对象是同一个对象,还要保证调用wait的对象和调用notify的对象是同一个对象
    Object object = new Object();
    Thread t1 = new Thread(()->{
        synchronized(object){
            try {
                object.wait();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    });
    
    a.wait();
    b.notify();//不能唤醒wait!
  7. 如果线程t1先执行了wait,线程t2后调用notify,此时notify会唤醒wait;但是如果线程t2先执行了notify,线程t1后调用wait,此时就错过了,特别注意即使没人调用wait,调用notify也不会有异常和副作用。
  8. 还有一个notifyAll。多个线程都在调用wait,notify是随机唤醒一个,而notifyAll则是全部唤醒,即使唤醒了所有的wait,这些wait就需要重新竞争锁,重新竞争锁的过程依然是串行的。
  9. wait和sleep的对比:
    对比:
    1.都是让线程进入阻塞等待的状态
    2.sleep是通过时间来控制何时唤醒的
      wait是由其他线程通过notify来唤醒的
    3.wait有个重载的版本,参数可以传时间,表示等待的最大时间(类似于join)

3.单例模式:

  1. 单例模式和工厂模式是常见的设计模式。
  2. 单例模式本质上借助了编程语言自身的语法特性,强行限制某个类,不能创建多个实例,有且只有一个实例。
  3. static修饰的成员(属性)变成了类成员(类属性),此时当属性变成类属性的时候此时及已经是单个实例了。更具体地说是类对象的属性,而类对象是通过JVM加载.class文件来的,其实类对象在JVM中也是单例。换句话说,JVM针对某个.class文件只会加载一次,也就只有一个类对象,类对象上面的static修饰的成员也就只有一份
  4. 饿汉模式:
    //饿汉模式
    class Singletion{
        //这个instance就是Singletion的唯一实例
        private static Singletion instance = new Singletion();
    
        //在类外可以通过getInstance来获取到实例
        public static Singletion getInstance(){
            return instance;
        }
    
        //把构造方法设置为private。此时类外就无法继续调用new实例
        private Singletion(){}
    
    }
    
    public class Demo8 {
        public static void main(String[] args) {
            //要继续使用这个实例
            Singletion singletion = Singletion.getInstance();
        }
    }
  5. 懒汉模式,比饿汉模式更加的高效:
    //懒汉模式(bug版本)
    class Singletion{
    
        private static Singletion instance = null;
    
        public static Singletion getInstance(){
            if(instance == null){
                //这里才是创建实例,
                // 首次调用getInatance才会触发,后续调用就立即返回
                instance = new Singletion();
            }
            return instance;
        }
    
        private Singletion(){}
    
    }
    
    public class Demo8 {
        public static void main(String[] args) {
            Singletion singletion = Singletion.getInstance();
        }
    }
  6. 上面写的懒汉和饿汉,谁的线程安全,谁的不安全?考虑这俩代码是否线程安全本质上是在考虑多个线程下同时调用getInstance,是否会有问题。饿汉模式中的getInstance只是单纯的读取数据的操作,不涉及修改,因此线程安全。而懒汉模式的getInstance既涉及到读又涉及到修改操作,则线程不安全。
  7. 如何修改让懒汉模式线程安全?加锁!,把多个操作打包成一个原子操作。
  8. //解决方案 1
    public static Singletion getInstance(){
        synchronized (Singletion.class){
            if(instance == null){
                instance = new Singletion();
            }
        }
        return instance;
    }
    //分析:这种加锁方式把线程不安全的问题解决了,但是又有了新的问题
      懒汉模式的线程不安全也只是实例创建之前(首轮调用的时候)才会触发线程不安全问题
      一旦实例创建好后,线程就安全了。导致当后续线程安全的时候仍然还得加锁,加锁开销挺大,代价大!
    //解决方案 2
    public static Singletion getInstance(){
        //实例创建之前,线程不安全,需要加
        //实例创建之后,线程安全,不需要加
        if(instance == null){//判断是否要加锁
            synchronized (Singletion.class){
                if(instance == null){//判断是否要创建实例
                    instance = new Singletion();
                }
            }
        }
        return instance;
    }
  9. 理解双重if判定,当多线程首次调用getInstance的时候,这些线程发现instance都为空,进入了外层if并且开始往下执行竞争锁,竞争成功的锁再来完成实例创建的操作;当这个实例创建好了之后,其他竞争到锁的线程就被里层的if挡住了,便不会再创建实例了。当再有第二批线程想来,直接就被挡在了外层if,直接就return了。
  10. 还有一个重要的问题,假设俩个线程同时调用getInstance,第一个线程拿到了锁,进入第二层if,开始new对象。new操作本质上分成三个步骤,先是申请内存,得到内存首地址;再调用构造方法,来初始化实例;最后把内存的首地址赋值给instance引用
  11. 这个场景下,编译器可能会进行指令重排序的优化操作,在单线程的角度下,步骤2和步骤3是可以调换顺序的(单线程的情况下,此时步骤2和步骤3谁先执行后执行效果都一样)。多线程情况下,假设此处触发了指令重排序,并且按照步骤1、3、2的顺序来执行,有可能线程t1执行了步骤1、3之后,执行步骤2之前,线程t2调用了getInstance,得到了不完全的对象,只是有内存,内存上的数据无效,这个getInstance就会认为instance非空,就直接返回了instance并且在后续可能就会针对instance进行解引用操作(使用里面的属性和方法),这就会出现异常!
  12. 这就是指令重排序带来的问题,要想解决这个问题,就是要禁止指令重排序,使用volatile,既能保证内存可见性,又能解决指令重排序的问题。
    //懒汉模式(完整版)
    class Singletion {
        //加上volatile就禁止了指令重排序
        private volatile static Singletion instance = null;
    
        public static Singletion getInstance() {
            //实例创建之前,线程不安全,需要加
            //实例创建之后,线程安全,不需要加
            if (instance == null) {//判断是否要加锁
                synchronized (Singletion.class) {
                    if (instance == null) {//判断是否要创建实例
                        instance = new Singletion();
                    }
                }
            }
            return instance;
        }
    
        private Singletion(){}
    }

 

如果对您有帮助的话,

不要忘记点赞+关注哦,蟹蟹

如果对您有帮助的话,

不要忘记点赞+关注哦,蟹蟹

如果对您有帮助的话,

不要忘记点赞+关注哦,蟹蟹

相关文章:

  • java自定义注解防重提交
  • 4-Arm PEG-Aldehyde,4ARM-PEG-CHO,四臂-聚乙二醇-醛基修饰蛋白质用试剂
  • C语言预处理、宏定义
  • Flink 成长之路专栏 - 导读目录
  • 软考高级系统架构设计师系列论文五十:论SOA在企业集成架构设计中的应用
  • spring boot企业网站设计与实现毕业设计源码211750
  • springboot基于JavaWeb的疫苗接种管理系统-JAVA.JSP【数据库设计、毕业设计、源码、开题报告】
  • vue组件间传值的六种方法
  • 2022牛客杭电多校dp题汇总
  • 记一次内网靶场渗透测试
  • 案例分析重点知识 变更文档配置收尾
  • Tomcat基本使用以及项目部署。
  • 编译redis5.0.4报错/usr/bin/ld: cannot find -latomic
  • 项目 - AES对称加密算法加密和解密设备联动码
  • ROS2在ROS1 的基础的改进点
  • 2018以太坊智能合约编程语言solidity的最佳IDEs
  • Android开源项目规范总结
  • AngularJS指令开发(1)——参数详解
  • Electron入门介绍
  • Java|序列化异常StreamCorruptedException的解决方法
  • JavaScript 一些 DOM 的知识点
  • JavaScript标准库系列——Math对象和Date对象(二)
  • Java深入 - 深入理解Java集合
  • Redis 懒删除(lazy free)简史
  • Spark学习笔记之相关记录
  • spring boot 整合mybatis 无法输出sql的问题
  • spring cloud gateway 源码解析(4)跨域问题处理
  • UMLCHINA 首席专家潘加宇鼎力推荐
  • 第十八天-企业应用架构模式-基本模式
  • 对话:中国为什么有前途/ 写给中国的经济学
  • 对象引论
  • 驱动程序原理
  • 微信小程序--------语音识别(前端自己也能玩)
  • Hibernate主键生成策略及选择
  • ​草莓熊python turtle绘图代码(玫瑰花版)附源代码
  • #ifdef 的技巧用法
  • #Linux(权限管理)
  • (04)odoo视图操作
  • (ZT)一个美国文科博士的YardLife
  • (二)构建dubbo分布式平台-平台功能导图
  • (分布式缓存)Redis分片集群
  • (附源码)ssm旅游企业财务管理系统 毕业设计 102100
  • (九十四)函数和二维数组
  • (力扣题库)跳跃游戏II(c++)
  • (切换多语言)vantUI+vue-i18n进行国际化配置及新增没有的语言包
  • (转)一些感悟
  • (转载)虚幻引擎3--【UnrealScript教程】章节一:20.location和rotation
  • (轉貼) 2008 Altera 亞洲創新大賽 台灣學生成果傲視全球 [照片花絮] (SOC) (News)
  • .bat文件调用java类的main方法
  • .FileZilla的使用和主动模式被动模式介绍
  • .form文件_SSM框架文件上传篇
  • .NET Framework 3.5中序列化成JSON数据及JSON数据的反序列化,以及jQuery的调用JSON
  • .net framework4与其client profile版本的区别
  • .net mvc 获取url中controller和action
  • .NET 发展历程