JavaEE:单例模式(饿汉模式和懒汉模式)精讲

前言

什么是单例模式?

其实用通俗的话就是程序猿约定俗成的一些东西,就比如如果你继承了一个抽象类,你就要重写里面的抽象方法,如果你实现了一个接口,你就要重写里面的方法。如果不进行重写,那么编译器就会报错。这其实就是一个规范。

而单例模式能保证某个类在程序中只存在唯一的一个实例,而不会创建出多个实例

那么,单例模式又分成“饿汉”和“懒汉”两种、

一.饿汉模式 

顾名思义,饿汉模式就是在类加载的时候,创建实例。

package thread;
//期待这个类能有唯一实例
public class hungryDemo {
    private static hungryDemo instance = new hungryDemo();

    public static hungryDemo getInstance() {
        return instance;
    }

    //把构造方法设置为私有,这样在类外就无法 new 出这个对象的实例了
    private hungryDemo() {

    }
}

代码解读:

1. 首先创建了一个 hungryDemo 类,里面有一个类方法和一个类变量

2. 我们将构造方法设置为了private,那么在类外就无法再针对 hungryDemo 再实例化类了


我们现在在类外,通过 hungryDemo提供的  public static hungryDemo getInstance 方法来进行调用,可以发现如下结果:

class Demo1 {
    public static void main(String[] args) {
        hungryDemo h1 = hungryDemo.getInstance();
        hungryDemo h2 = hungryDemo.getInstance();
        System.out.println(h1 == h2);

    }
}

运行结果:

可以发现,两者获取到的类对象引用是一致的,那么单例模式的饿汉版本就创建好了。

二.懒汉模式 

🎈单线程版本

我们的预期结果是不变的,那就是要实现单例模式,也就是这个类 只能被实例化一次!!!

那么懒汉模式顾名思义,也就是类加载的时候不创建实例,第一次使用的时候才创建实例。

那么我们可以写出以下代码:

package thread;

public class lazyDemo {
    private static lazyDemo instance = null;  
    public static lazyDemo getInstance() {
        /**
         * 只有调用该方法的时候,才创建对象
         */
                if (instance == null) {
                    instance = new lazyDemo();
                }               
        return instance;
    }
    private lazyDemo() {

    }
}

代码解读:

🍺首先,设置类成员变量 instance 为 null,当第一次使用getInstance()的时候才进行创建对      象。

🍺其次,跟饿汉模式一样,将类的构造方法设置为 private ,类外无法再次创建对象。

🍺最后,在getInstance方法中判断 instance 是否为空,为空那就创建对象。为空说明已经        被调用一次了,那么就直接返回 instance 引用。

🎈🎈多线程版本 1

在以上的单线程版本中,我们不难发现以下问题:

假设现在有两个线程,他们是按照如下的顺序来执行的:

那么此时的代码就会出现问题: t1 线程首先判断了 instance 是否为空,此时 t2 线程来运行了,也判断 instance 是否为空,紧接着 instance不为空,然后就创建了对象! 然后再回到 t1 线程中,又要进行创建对象。  此时问题已经很明显了,那就是 由于if代码块在多线程中的执行顺序问题导致的

更精简一下:

就是 instance = new lazyDemo() 是写操作, instance == null 是读操作,在多线程中,如果一段代码即涉及读操作,又设计写操作,那么就很容易出现问题!!!


🍭解决办法:

  当一段代码是因为读写操作出BUG,我们首先想到的就是加锁。也就是在我写的时候,你不要       读。我读的时候,你不要写。

synchronized 是一种内置的 Java 关键字,它用于实现线程的同步。当一个线程进入synchronized块或方法时,它获得了锁,这会阻止其他线程同时进入相同的synchronized块或方法,从而确保了共享资源的互斥访问。

修改代码如下:

package thread;

public class lazyDemo {
    private static lazyDemo instance = null;  
    public static lazyDemo getInstance() {
        /**
         * 只有调用该方法的时候,才创建对象
         */
            synchronized (lazyDemo.class) {   //1. 加锁解决的是线程安全问题(确保是单例模式,只new一次)
                if (instance == null) {
                    instance = new lazyDemo();
                }
            }
        return instance;
    }

    private lazyDemo() {

    }
} 

对于对象lazyDemo.class,实际上就是lazyDemo这个类,也就是对类进行加锁。

此时加锁之后,当t1线程进行读写操作的时候,t2线程再次进行访问就只能进行阻塞。

此时t1就可以放心创建出一个对象出来,此时t2再进行调用方法的时候,instance 不为空,就直接返回 t1 创建好的对象引用。 这时候就确保了只创建出一个实例。

🎈🎈🎈 多线程版本2

其实,多线程版本1 还是有问题的,我们发现:如果t1 线程加锁后创建好了对象,其他线程(t2,t3,t4.........)在进行访问的时候,首先就要进行加锁操作。 也就是每次访问都要进行加锁,这是一个资源开销非常大的操作。

深入探究一下,我们发现其他线程(t2,t3,t4.........)在进行访问的时候,只需要判断当前的对象是否被创建好了即可。如果被创建好了,那么就直接返回对象引用。如果没有被创建好,再进行加锁创建对象。

修改代码如下:

public class lazyDemo {
    private static lazyDemo instance = null; 
    public static lazyDemo getInstance() {
        /**
         * 只有调用该方法的时候,才创建对象
         */
        if(instance == null) {   //2. if判断解决的是多次加锁,加锁频率太高的问题
            synchronized (lazyDemo.class) {   //1. 加锁解决的是线程安全问题(确保是单例模式,只new一次)
                if (instance == null) {
                    instance = new lazyDemo();
                }
            }
        }
        return instance;
    }

    private lazyDemo() {

    }
}

在多线程中,这两个 if 的作用大不相同!!!

修改后,我们发现如果 t1 线程创建好了对象, 此时其他线程(t2,t3,t4.........)在进行调用的时候,首先判断了instance 是否为空,不为空就说明已经创建好了对象~

🎈🎈🎈🎈多线程版本3

其实到现在,这个懒汉模式的单例代码还是有问题!!!

 在多线程下,要考虑到编译器的优化问题,当编译器没有按照我们的逻辑进行操作的时候,那么就会出现问题。

在此代码中,new 操作可以分为以下三步:

1.申请内存空间(一定先执行),获取到内存地址  

2.在内存空间上构造对象(构造方法)

3.把内存的地址,赋值给 instance 引用

在单线程环境下,执行那种顺序都无所谓,但是如果在多线程环境下,就可能出现问题:

假设是按照 1 3 2 的顺序来执行,当 1 和 3 操作执行完的时候,instance 已经非空了,只是内存空间上还没有构造对象 / 方法,此时instance 指向的是一个还没初始化的非法对象。 此时此刻 t2 进行访问,判断 instanc 是不为空的,然后就返回了一个还没初始化的非法对象,进一步 t2 线程就有可能访问 instance 里面的属性和方法。此时就出现了问题了。

这个问题就是指令重排序问题,解决办法就是让 instance 加入上volatile 关键字,此时就避免了指令重排序问题。

    //3.加 volatile是为了解决new 操作的指令重排序问题    
private volatile static lazyDemo instance = null; 

此时的代码就会严格按照 1  2  3 的顺序执行。


总结:单例模式是一个约定俗成的规范,保证一个类只能实例化一个对象。饿汉模式在多线程和单线程都没有问题,因为一开始它就创建好了对象。 而懒汉模式的多线程版本会出现以下三个问题:1. 线程安全问题( 确保只new 一次)2. 多次重复加锁的问题  3. 指令重排序问题。

希望以上的解决办法对你有所帮助!!!

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

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

相关文章

Android BottomSheetBehavior(底部弹窗)

目录 一、BottomSheetBehavior 介绍 二、BottomSheetBehavior 基本使用 2.1 在 CoordinatorLayout 中添加底部工作表: 2.2 在代码中获取 BottomSheetBehavior 实例: 2.3 设置工作表的状态,如展开、折叠等 2.4 工作表的状态 三、Bottom…

redis-学习笔记(Jedis zset 简单命令)

zadd & zrange zadd , 插入的第一个参数是 zset , 第二个参数是 score, 第三个参数是 member 成员 内部依据 score 排序 zrange 返回 key 对应的 对应区间内的值 zrangeWithScore 返回 key 对应的 对应区间内的值和分数 示例代码 zcard 返回 key 对应的 zset 的长度 示例代…

cannot add foreign key constraint问题的解决

cannot add foreign key constraint问题的解决 01 发生场景 当我给mysql数据库表加外键的时候 02 异常发生原因及其解决方式 添加外键时要注意几个关键点,否则很容易失败 1.数据类型一定要一致 添加外键的项和参考项的数据类型一定要一样,这里我报错…

05-命令模式

意图(GOF定义) 将一个请求封装为一个对象,从而使你可用不同的请求对客户端进行参数化,对请求排队或者记录日志,以及可支持撤销的操作。 理解 命令模式就是把一些常用的但比较繁杂的工作归类为成一组一组的动作&…

深入理解Java虚拟机---Java内存模型

JMM Java内存模型主内存和工作内存volatile Java内存模型 Java内存模型是Java虚拟机规范中试图定义一种Java内存模型(JMM)来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台上都能达到一致的内存访问效果。可以理解为JMM定义一套在多线程读写共…

一进一出两线制(三线制)模拟量高速信号(50KHz)隔离变送器

一进一出两线制(三线制)模拟量高速信号(50KHz)隔离变送器 型号:JSD TA-2311F系列 产品特点: ◆小体积,低成本,标准 DIN35mm 导轨安装方式 ◆三端隔离(输入、输出、工作电源间相互隔离) ◆高速信号采集,隔离等(-3dB,Min≤ 3.5 uS) ◆高精度等级(0.1% F.S&…

Netty详细文档

Netty教程 文章目录 Netty教程 Netty简介Netty 的介绍Netty 的应用场景互联网行业游戏行业大数据领域其它开源项目使用到 Netty Netty 的学习资料参考 Java BIO编程I/O 模型BIO、NIO、AIO 使用场景分析Java BIO 基本介绍Java BIO 工作机制Java BIO 应用实例问题分析 Java NIO编…

curl+postman 在java开发中的使用(提高效率)

概念 curl 是一个常用的命令行工具,用于发送各种类型的 HTTP 请求,包括 GET、POST、PUT、DELETE 等。它也可以用来下载文件、上传文件、设置 cookie、发送 multipart/form-data 等等。 使用 调用post接口 实际中的接口: curl --location…

Reactor线程模型详解

文章目录 传统的阻塞式 I/OReactor 模式单 Reactor 单线程单Reactor多线程主从Reactor多线程 在目前的线程模型中一种是传统阻塞的I/O模型,一种是Reactor线程模型。 传统的阻塞式 I/O 为了同时处理多个客户端的请求,服务端为每一个连接都会分配一个新的…

XSS防御:内容安全策略 CSP工作原理、配置技巧与最佳实践

前言 公司部门安全合规改造计划,要求所有的Web站点统一添加CSP规则。对于CSP机制我只是之前在应付面试的时候背过相关的概念,并没有真正在项目中实践过。所以希望借助本次改造任务好好理解并实践CSP机制。 什么是CSP CSP的全称是 Content Security Po…

yolov7简化网络yaml配置文件

本篇介绍如何简化yolov7的网络配置文件 先上一张简化后的网络结构图: 了解v7的都知道,配置文件中网络层数多达100多层,不过并不复杂,相似的模块比较多,可以看到简化后配置文件的层数也就31层。 简化前的配置文件&…

基于ssm社区管理与服务的设计与实现论文

目录 摘 要 1 Abstract 2 第一章 绪论 3 1.1研究背景 3 1.2 研究现状 3 1.3 研究内容 4 第二章 系统关键技术 5 2.1 Java简介 5 2.2 MySql数据库 5 2.3 B/S结构 6 2.4 Tomcat服务器 6 第三章 系统分析 7 3.1可行性分析 7 3.1.1技术可行性 7 3.1.2经济可行性 7 3.1.3运行可行性…

mysql的redolog、undo、binlog的作用

概览: MySQL三大日志包括:undolog,redo log,binlog,它们分别有以下作用: undolog:是Innodb存储引擎事务生成的日志。用于事务的回滚和MVCC,保证了事务的原子性。 redo log&#x…

提升数据采集技能:用 Axios 实现的 Twitter 视频下载器全面解析

引入 在当今数据驱动的时代,高效的数据采集是实现成功数据科学项目的关键。数据采集不仅涉及到数据的获取,还包括数据的清洗、转换、存储和分析等多个环节。Twitter作为全球最大的社交媒体平台之一,蕴含着丰富的信息和海量的多媒体内容&…

Liunx高级系统设计9-线程间同步与互斥

同步与互斥的概念 互斥:同一时间,只能有一个任务(进程或线程)执行,谁先执行不确定。 同步:同一时间,只能有一个任务(进程或线程)执行,有顺序的执行。 同步…

python+pytest接口自动化(15)-日志管理模块loguru简介

python自带日志管理模块logging,使用时可进行模块化配置, 但logging配置起来比较繁琐,且在多进行多线程等场景下使用时,如果不经过特殊处理,则容易出现日志丢失或记录错乱的情况。 python中有一个用起来非常简便的第…

成都工业学院2021级操作系统专周课程设计FCFS,SSTF,SCAN,LOOK算法的实现

运行环境 操作系统&#xff1a;Windows 11 家庭版 运行软件&#xff1a;CLion 2023.2.2 源代码文件 #include <iostream> #include <vector> #include <algorithm> #include <random> using namespace std;// 生成随机数 int generateRandomNumber…

理解 Proxy 和 Object.defineProperty:提升你的 JavaScript 技能(下)

&#x1f90d; 前端开发工程师&#xff08;主业&#xff09;、技术博主&#xff08;副业&#xff09;、已过CET6 &#x1f368; 阿珊和她的猫_CSDN个人主页 &#x1f560; 牛客高级专题作者、在牛客打造高质量专栏《前端面试必备》 &#x1f35a; 蓝桥云课签约作者、已在蓝桥云…

leetcode(平衡二叉树)

https://leetcode.cn/problems/balanced-binary-tree/description/ 这题的思路分成子问题就是计算左右子树的高度然后相减看看是不是大于1的就可以了&#xff0c;所以代码如下 int _isBalanced(struct TreeNode* root) {if(root NULL){return 0;}int leftdepth _isBalanced(…

详细说说vuex

Vuex 是什么 Vuex有几个属性及作用注意事项vuex 使用举例Vuex3和Vuex4有哪些区别 创建 Store 的方式在组件中使用 Store辅助函数的用法响应式的改进Vuex4 支持多例模式 Vuex 是什么 Vuex是一个专门为Vue.js应用设计的状态管理构架&#xff0c;它统一管理和维护各个Vue组件的可…