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

news2024/12/23 16:23:26

前言

什么是单例模式?

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

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

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

一.饿汉模式 

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

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. 指令重排序问题。

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

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

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

相关文章

ajax和Axios快速入门

什么是ajax 概念: Asynchronous JavaScript And XML,异步的JavaScrip和XML,重点在异步。 作用: 1,数据交互,可以通过ajax给服务器发送请求,并获取服务器响应的数据。 2,异步交互&am…

一些程序源码及教程的网站合集~

很多时候我们需要一个快速上手的code demo及教程,除了最常用的【github】,一些中文网站可能会帮助我们更好上手~ 这里提供几个中文网站参考: 【51CTO】: Python 动态手势识别系统hmm 手势识别opencv_mob64ca140d96d9的技术博客…

Vue3-09-条件渲染-v-show 的基本使用

v-show 的作用 v-show 可以根据条件表达式的值【展示】或【隐藏】html 元素。v-show 的特点 v-show 的实现方式是 控制 dom 元素的 css的 display的属性, 因此,无论该元素是否展示,该元素都会正常渲染在页面上, 当v-show 的 条件…

嵌入式系统未来的发展趋势走向???

人工智能和机器学习应用 模型优化: 为了在资源有限的嵌入式系统上运行,将会看到更多的努力投入到精简、优化和量化模型,以适应边缘计算的环境。 边缘推理: 嵌入式设备将更多地执行本地推理,而不是将所有数据发送到云端…

vue的computed中的getter和setter

vue的computed中的getter和setter 定义getter写法setter写法 定义 computed 中可以分成 getter(读取) 和 setter(设值),一般情况下是没有 setter 的,computed 预设只有 getter,也就是只能读取&a…

9.1 字符设备驱动开发

一、字符设备驱动简介 字符设备是 Linux 驱动中最基本的一类设备驱动,字符设备就是一个一个字节,按照字节流进行读写操作的设备,读写数据是分先后顺序的。比如我们最常见的点灯、按键、 IIC、 SPI,LCD 等等都是字符设备&#xff0…

c语言二维数组之字符数组

介绍: 本篇介绍 c语言二维数组之字符数组 其实他和前两种数组 c语言二维数组之整型数组 c语言二维数组之浮点型数组 差不多 就有两处不同 所以 本篇文章重点讲那两点不同 c语言二维数组之字符数组: 第一点不同: 第一个不同点就是 它存储的…

小航助学2023年9月电子学会Scratch四级真题(含题库答题软件账号)

需要在线模拟训练的题库账号请点击 小航助学编程在线模拟试卷系统(含题库答题软件账号) 单选题3.00分 删除编辑附件图文 答案:A 第1题角色为一个紫色圆圈,运行程序后,舞台上的图案是?( ) A…

imazing是什么软件

imazing是什么软件 iMazing 是世界上最值得信赖的软件,可以将您的信息、音乐、文件和数据从 iPhone 或 iPad 传输到您的 Mac 或 PC。 获得备份、数据提取、媒体和文件传输的强大能力,以及更多 iMazing 功能。 iMazing是一款第三方的苹果iOS设备管理软件。 iMazing- 2 Mac-安装…

CPython(将Python编译为so)

环境 先配一下环境,我使用的是python3.8.5 pip install Cython 编译过程 我们准备一个要编译的文件 test.py def xor(input_string): output_string "" for char in input_string: output_string chr(ord(char) ^ 0x66) return output_string …

对于双显卡电脑,如何分辨现在用的是独立显卡还是集成显卡?

一、问题描述 台式电脑本身自带了集成显卡,然后又购买了一块NVIDIA的独立显卡。 现在,就有疑问了,如何判断你的显示器连接的是独立显卡还是集成显卡呢? 二、NVIDIA双显卡机型 1、在桌面右下角,选择NVIDIA图标&…

【字符串】ABC324E

退役啦,接下来的博客全是图一乐啦 E - Joint Two Strings 题意 思路 统计两个指针的方案数一定是枚举一个,统计另一个 然后因为拼起来之后要包含 t 这个字符串,隐隐约约会感觉到和前缀后缀子序列有关 考虑预处理每个 s[i] 的最长公共前…

C语言-WIN32API介绍

Windows API 从第一个32位的Windows开始就出现了,就叫做Win32API.它是一个纯C的函数库,就和C标准库一样,使你可以写Windows应用程序过去很多Windows程序是用这个方式做出来的 main()? main()成为C语言的入口函数其实和C语言本身无关&…

boost1.55 安装使用教程 windows

第一步 :首先在boost官网上下载库压缩包 添加链接描述 选择自己需要的版本进行下载 解压后执行booststrap.bat 用来生成创建b2.exe 和bjam.exe 拓展:.\b2 --help 了解一下有哪些参数可以配置 默认b2.exe编译后,链接到项目如果出现如下错误…

Docker Compose入门:打造多容器应用的完美舞台

Docker Compose 是一个强大的工具,它允许开发者通过简单的 YAML 文件定义和管理多容器的应用。本文将深入讨论 Docker Compose 的基本概念、常用命令以及高级应用场景,并通过更为丰富和实际的示例代码,助您轻松掌握如何通过 Docker Compose 打…

docker-compose Nginx Proxy Manager

Nginx Proxy Manager前言 Nginx Proxy Manager(以下简称 NPM)就是一个 Nginx 的代理管理器,它最大的特点是简单方便。 美观且安全的管理界面,基于表格轻松创建转发域、重定向、流和 404 主机,而无需了解 Nginx使用 Lets Encrypt 免费 SSL 或提供您自己的自定义 SSL 证书主…

详细说说vuex

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

【从零开始学习JVM | 第八篇】学习垃圾回收算法 和 垃圾回收器

前言: 现代编程语言通常采用垃圾回收机制来自动管理内存。垃圾回收机制是一种自动化的内存管理技术,可以在程序运行时自动识别和回收不再使用的内存,从而减少内存泄漏和其他内存相关问题的发生。 本文将介绍垃圾回收算法和垃圾回收器的相关…

【算法与数据结构】37、LeetCode解数独

文章目录 一、题目二、解法三、完整代码 所有的LeetCode题解索引,可以看这篇文章——【算法和数据结构】LeetCode题解。 一、题目 二、解法 思路分析:本题也是一道困难题,难点在于如何构建数独棋盘,如何检查棋盘的合法性&#xff…

智能优化算法应用:基于树种算法3D无线传感器网络(WSN)覆盖优化 - 附代码

智能优化算法应用:基于树种算法3D无线传感器网络(WSN)覆盖优化 - 附代码 文章目录 智能优化算法应用:基于树种算法3D无线传感器网络(WSN)覆盖优化 - 附代码1.无线传感网络节点模型2.覆盖数学模型及分析3.树种算法4.实验参数设定5.算法结果6.参考文献7.MA…