设计模式(四)创建者模式之单例模式

单例模式

  • 单例设计模式
  • 单例模式的结构
  • 单例模式的实现
    • 饿汉式-方式1(静态变量方式)
    • 饿汉式-方式2(静态代码块方式)
    • 懒汉式-方式1(线程不安全)
    • 懒汉式-方式2(线程安全) synchronized 关键字
    • 懒汉式-方式3(双重检查锁)
    • 懒汉式-方式4(静态内部类方式)
    • 枚举方式
    • 存在的问题
      • 破坏单例模式:
        • 序列化反序列化
        • 反射
    • 问题的解决
      • 序列化、反序列方式破坏单例模式的解决方法
      • 反射方式破解单例的解决方法

创建型模式的主要关注点是“怎样创建对象?”,它的主要特点是“将对象的创建与使用分离”。
这样可以降低系统的耦合度,使用者不需要关注对象的创建细节。

单例设计模式

单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。

这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。

单例模式的结构

单例模式的主要有以下角色:

  • 单例类。只能创建一个实例的类
  • 访问类。使用单例类

单例模式的实现

单例设计模式分类两种:

  • 饿汉式:类加载就会导致该单实例对象被创建
  • 懒汉式:类加载不会导致该单实例对象被创建,而是首次使用该对象时才会创建

饿汉式-方式1(静态变量方式)

package com.lx.design.creator.single;/*** TODO 添加描述** @author lx* @date 2024/6/15 12:46* 单例模式-饿汉式*  静态变量创建类的对象*/
public class SingleDemo {//私有构造方法private    SingleDemo (){}//在成员位置创建该类的对象private static SingleDemo singleDemo=new SingleDemo();//对外提供静态方法获取该对象public static SingleDemo getSingleDemo(){return singleDemo;}
}

说明:
​ 该方式在成员位置声明SingleDemo 类型的静态变量,并创建SingleDemo 类的对象instance。instance对象是随着类的加载而创建的。如果该对象足够大的话,而一直没有使用就会造成内存的浪费

饿汉式-方式2(静态代码块方式)

package com.lx.design.creator.single;/*** TODO 添加描述** @author lx* @date 2024/6/15 12:46* 单例模式-饿汉式*  在静态代码块中创建该类对象*/
public class SingleDemo2 {//私有构造方法private SingleDemo2(){}//在成员位置创建该类的对象private static SingleDemo2 instance ;static  {instance=new SingleDemo2();}//对外提供静态方法获取该对象public static  SingleDemo2 getSingleDemo(){return instance;}
}

说明:
该方式在成员位置声明SingleDemo2 类型的静态变量,而对象的创建是在静态代码块中,也是对着类的加载而创建。所以和饿汉式的方式1(静态变量方式)基本上一样,当然该方式也存在内存浪费问题

懒汉式-方式1(线程不安全)

package com.lx.design.creator.single;/*** TODO 添加描述** @author lx* @date 2024/6/15 12:46* 单例模式-懒汉式* 线程不安全*/
public class SingleDemo3 {//私有构造方法private SingleDemo3() {}//在成员位置创建该类的对象private static SingleDemo3 instance;//对外提供静态方法获取该对象public  static SingleDemo3 getSingleDemo() {if (instance == null) {instance = new SingleDemo3();}return instance;}
}

说明:
从上面代码我们可以看出该方式在成员位置声明SingleDemo3类型的静态变量,并没有进行对象的赋值操作,那么什么时候赋值的呢?当调用getSingleDemo()方法获取SingleDemo3类的对象的时候才创建SingleDemo3类的对象,这样就实现了懒加载的效果。但是,如果是多线程环境,会出现线程安全问题。

懒汉式-方式2(线程安全) synchronized 关键字

package com.lx.design.creator.single;/**
* TODO 添加描述
*
* @author lx
* @date 2024/6/15 12:46
* 单例模式-懒汉式
* 线程 安全
*/
public class SingleDemo4 {//私有构造方法private SingleDemo4() {}//在成员位置创建该类的对象private static SingleDemo4 instance;//对外提供静态方法获取该对象public static synchronized SingleDemo4 getSingleDemo() {if (instance == null) {instance = new SingleDemo4();}return instance;}
}

说明:
该方式也实现了懒加载效果,同时又解决了线程安全问题。但是在getSingleDemo()方法上添加了synchronized关键字,导致该方法的执行效果特别低。从上面代码我们可以看出,其实就是在初始化instance的时候才会出现线程安全问题,一旦初始化完成就不存在了。

懒汉式-方式3(双重检查锁)

再来讨论一下懒汉模式中加锁的问题,对于 getSingleDemo() 方法来说,绝大部分的操作都是读操作,读操作是线程安全的,所以我们没必让每个线程必须持有锁才能调用该方法,我们需要调整加锁的时机。由此也产生了一种新的实现模式:双重检查锁模式

package com.lx.design.creator.single;/*** TODO 添加描述** @author lx* @date 2024/6/15 12:46* 单例模式-懒汉式* 线程 安全*/
public class SingleDemo5 {//私有构造方法private SingleDemo5() {}//在成员位置创建该类的对象private static SingleDemo5 instance;//对外提供静态方法获取该对象public static synchronized SingleDemo5 getSingleDemo() {//第一次判断,如果instance不为null,不进入抢锁阶段,直接返回实例if(instance == null) {synchronized (SingleDemo5.class) {//抢到锁之后再次判断是否为nullif(instance == null) {instance = new SingleDemo5();}}}return instance;}
}

双重检查锁模式是一种非常好的单例实现模式,解决了单例、性能、线程安全问题,上面的双重检测锁模式看上去完美无缺,其实是存在问题,在多线程的情况下,可能会出现空指针问题,出现问题的原因是JVM在实例化对象的时候会进行优化指令重排序操作。

要解决双重检查锁模式带来空指针异常的问题,只需要使用 volatile 关键字, volatile 关键字可以保证可见性和有序性。

更改 instance 成员变量被volatile修饰

package com.lx.design.creator.single;/*** TODO 添加描述** @author lx* @date 2024/6/15 12:46* 单例模式-懒汉式* 线程 安全*/
public class SingleDemo5 {//私有构造方法private SingleDemo5() {}//在成员位置创建该类的对象private static  volatile SingleDemo5 instance;//对外提供静态方法获取该对象public static  SingleDemo5 getSingleDemo() {//第一次判断,如果instance不为null,不进入抢锁阶段,直接返回实例if(instance == null) {synchronized (SingleDemo5.class) {//抢到锁之后再次判断是否为nullif(instance == null) {instance = new SingleDemo5();}}}return instance;}
}

小结:
添加 volatile 关键字之后的双重检查锁模式是一种比较好的单例实现模式,能够保证在多线程的情况下线程安全也不会有性能问题。

懒汉式-方式4(静态内部类方式)

静态内部类单例模式中实例由内部类创建,由于 JVM 在加载外部类的过程中, 是不会加载静态内部类的, 只有内部类的属性或者方法被调用时才会被加载, 并初始化其静态属性。静态属性由于被 static 修饰,保证只被实例化一次,并且严格保证实例化顺序。

package com.lx.design.creator.single;/*** TODO 添加描述** @author lx* @date 2024/6/15 12:46* 单例模式-静态内部类方式*/
public class SingleDemo6 {//私有构造方法private SingleDemo6() {}private static class SingletonHolder {private static final SingleDemo6 INSTANCE = new SingleDemo6();}//对外提供静态方法获取该对象public static SingleDemo6 getInstance() {//访问内部类中的属性或者方法。才会加载内部类。//并初始化其静态属性。静态属性由于被 `static` 修饰,保证只被实例化一次return SingletonHolder.INSTANCE;}
}

说明:
第一次加载SingleDemo6 类时不会去初始化INSTANCE,只有第一次调用getInstance,虚拟机加载SingletonHolder。 并初始化INSTANCE,这样不仅能确保线程安全,也能保证 Singleton 类的唯一性。

小结:
静态内部类单例模式是一种优秀的单例模式,是开源项目中比较常用的一种单例模式。在没有加任何锁的情况下,保证了多线程下的安全,并且没有任何性能影响和空间的浪费。

枚举方式

枚举类实现单例模式是极力推荐的单例实现模式,因为枚举类型线程安全的,并且只会装载一次,设计者充分的利用了枚举的这个特性来实现单例模式,枚举的写法非常简单,而且枚举类型是所用单例实现中唯一一种不会被破坏的单例实现模式。

/*** 枚举方式*/
public enum Singleton {INSTANCE;
}

说明:
​ 枚举方式属于恶汉式方式。

存在的问题

破坏单例模式:

使上面定义的单例类(Singleton)可以创建多个对象,枚举方式除外。有两种方式,分别是序列化和反射。

序列化反序列化
package com.lx.design.creator.single;import java.io.Serializable;/*** TODO 添加描述** @author lx* @date 2024/6/15 12:46* 单例模式-静态内部类方式*/
public class SingleDemo6 implements Serializable {//私有构造方法private SingleDemo6() {}private static class SingletonHolder {private static final SingleDemo6 INSTANCE = new SingleDemo6();}//对外提供静态方法获取该对象public static SingleDemo6 getInstance() {//访问内部类中的属性或者方法。才会加载内部类。//并初始化其静态属性。静态属性由于被 `static` 修饰,保证只被实例化一次return SingletonHolder.INSTANCE;}
}

测试 Test1 类

package com.lx.design.creator.single;import java.io.*;/*** TODO 添加描述** @author lx* @date 2024/6/15 12:46* 单例模式-静态内部类方式*/
public class Test1 implements Serializable {public static void main(String[] args) throws Exception {//往文件中写对象writeObject2File();//从文件中读取对象SingleDemo6 s1 = readObjectFromFile();SingleDemo6 s2 = readObjectFromFile();//判断两个反序列化后的对象是否是同一个对象System.out.println(s1 == s2);}private static SingleDemo6 readObjectFromFile() throws Exception {//创建对象输入流对象ObjectInputStream ois = new ObjectInputStream(new FileInputStream("D:\\SingleDemo6.txt"));//第一个读取Singleton对象SingleDemo6 instance = (SingleDemo6) ois.readObject();return instance;}public static void writeObject2File() throws Exception {//获取Singleton类的对象SingleDemo6 instance = SingleDemo6.getInstance();//创建对象输出流ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D:\\SingleDemo6.txt"));//将instance对象写出到文件中oos.writeObject(instance);}}

上面代码运行结果是false,表明序列化和反序列化已经破坏了单例设计模式

反射

**Test2 **

  package com.lx.design.creator.single;import java.io.*;
import java.lang.reflect.Constructor;/*** TODO 添加描述** @author lx* @date 2024/6/15 12:46* 单例模式-静态内部类方式*/
public class Test2 implements Serializable {public static void main(String[] args) throws Exception {//获取Singleton类的字节码对象Class clazz = SingleDemo5.class;//获取Singleton类的私有无参构造方法对象Constructor constructor = clazz.getDeclaredConstructor();//取消访问检查constructor.setAccessible(true);//创建Singleton类的对象s1SingleDemo5 s1 = (SingleDemo5) constructor.newInstance();//创建Singleton类的对象s2SingleDemo5 s2 = (SingleDemo5) constructor.newInstance();//判断通过反射创建的两个Singleton对象是否是同一个对象System.out.println(s1 == s2);}
}

上面代码运行结果是false,表明序列化和反序列化已经破坏了单例设计模式

注意:枚举方式不会出现这两个问题。

问题的解决

序列化、反序列方式破坏单例模式的解决方法

在Singleton类中添加readResolve()方法,在反序列化时被反射调用,如果定义了这个方法,就返回这个方法的值,如果没有定义,则返回新new出来的对象。

package com.lx.design.creator.single;import java.io.Serializable;/*** TODO 添加描述** @author lx* @date 2024/6/15 12:46* 单例模式-静态内部类方式*/
public class SingleDemo6 implements Serializable {//私有构造方法private SingleDemo6() {}private static class SingletonHolder {private static final SingleDemo6 INSTANCE = new SingleDemo6();}//对外提供静态方法获取该对象public static SingleDemo6 getInstance() {//访问内部类中的属性或者方法。才会加载内部类。//并初始化其静态属性。静态属性由于被 `static` 修饰,保证只被实例化一次return SingletonHolder.INSTANCE;}/*** 下面是为了解决序列化反序列化破解单例模式*/private Object readResolve() {return SingletonHolder.INSTANCE;}
}

再次执行Test1 发现返回true

反射方式破解单例的解决方法

package com.lx.design.creator.single;/*** TODO 添加描述** @author lx* @date 2024/6/15 12:46* 单例模式-懒汉式* 线程 安全*/
public class SingleDemo5 {//判断是否已经实例化对象标识private static  boolean flag =false;//私有构造方法private SingleDemo5() {//判断是否已经实例化对象if (flag){throw new RuntimeException();}//更改  flag =true;}//在成员位置创建该类的对象private static  volatile SingleDemo5 instance;//对外提供静态方法获取该对象public static   SingleDemo5 getSingleDemo() {//第一次判断,如果instance不为null,不进入抢锁阶段,直接返回实例if(instance == null) {synchronized (SingleDemo5.class) {//抢到锁之后再次判断是否为nullif(instance == null) {instance = new SingleDemo5();}}}return instance;}
}

说明:
这种方式比较好理解。当通过反射方式调用构造方法进行创建创建时,直接抛异常。不运行此中操作。

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

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

相关文章

注册安全分析报告:PingPong

前言 由于网站注册入口容易被黑客攻击,存在如下安全问题: 暴力破解密码,造成用户信息泄露短信盗刷的安全问题,影响业务及导致用户投诉带来经济损失,尤其是后付费客户,风险巨大,造成亏损无底洞 …

vscode-关闭ts与js语义校验

1.ts与js语义校验 TypeScript(TS)和JavaScript(JS)在语义校验方面有很大的不同。TypeScript是一种静态类型检查的编程语言,它是JavaScript的一个超集,为JavaScript添加了类型系统和其他一些特性。而JavaScr…

12.爬虫---PyMysql安装与使用

12.PyMysql安装与使用 1.安装 PyMySQL2.使用PyMySQL2.1创建数据表2.2连接数据库2.3增加数据2.4修改数据2.5查询数据2.6删除数据2.7关闭连接 3.总结 1.安装 PyMySQL PyMySQL是Python中用于连接MySQL数据库的库,安装起来非常简单。通常情况下,只需要在命令…

从零开始:精通基于大型语言模型(LLM)的Agent应用开发

一、引言 随着人工智能技术的飞速发展,大型语言模型(Large Language Model,简称LLM)已经成为自然语言处理(NLP)领域的核心技术之一。这些模型,如GPT、BERT等,通过大量的文本数据训练…

八个精品ETL工具,总有一款适合您的业务需求!

在数字经济高速发展的今天,数据的价值愈发凸显。ETL(Extract, Transform, Load)工具作为数据集成的关键一环,不仅帮助企业高效管理海量数据,还能为商业决策提供实时洞察。本文将深入探讨目前市场上的8款领先ETL工具&am…

大模型API和私有化部署的区别与联系

大模型 API(Application Programming Interface)和私有化部署是使用大规模机器学习模型的两种主要方式。它们有各自的优点和缺点,适用于不同的应用场景。以下是它们的区别与联系: 大模型 API 特点: 即用即用&#x…

Java—装饰器模式

介绍 装饰器模式 装饰器模式(Decorator Pattern)是一种结构型设计模式,它允许你动态地将行为添加到现有的对象中,而无需修改其代码。装饰器模式提供了比继承更灵活的功能扩展方式。 主要角色 Component:定义一个对…

变量不自动初始化

代码: /*《AVR专题精选》随书例程2.编程技巧项目:不对变量进行初始化文件:main.c说明:演示不对变量进行默认初始化的方法。在proteus仿真例程中,按下按键,就可以看到两个变量输出结果的变化。作者&#xf…

今日事、今日毕,任务管理系统

使用 C 实现的今日事,今日毕,任务管理系统。

解决上一篇误删问题的改进(增加线程标识校验)

本文将基于上一篇文章介绍如何通过改进 Redis 分布式锁的实现来解决误删问题。 分布式锁的改进实现 1. 误删问题的原因 在原始实现中,分布式锁通过 Redis 的 setIfAbsent 方法获取锁,并通过 delete 方法释放锁。然而,在某些情况下&#xf…

web前端defer:深度解析与实用指南

web前端defer:深度解析与实用指南 在web前端开发中,defer是一个关键的属性,它影响着脚本的加载和执行方式。然而,对于许多开发者来说,defer的真正含义和用法却常常带来困惑。本文将通过四个方面、五个方面、六个方面和…

Python的print,input与注释的使用

1.print的使用 2.input的使用 3.如何注释 1.print的使用 1.1建立俩个变量a,b,直接把变量放在print的括号里面就会打印其的值。 1.2print可以同时打印多个,打印ab的值与字符串‘11’。 1.3先用chr()函数去获取对应97…

剖析 Kafka 消息丢失的原因

文章目录 前言一、生产者导致的消息丢失的场景场景1:消息太大解决方案 :1、减少生产者发送消息体体积2、调整参数max.request.size 场景2:异步发送机制解决方案 :1、使用带回调函数的发送方法 场景3:网络问题和配置不当…

定义仅限关键字参数

定义仅限关键字参数 Python里的函数不光支持通过有序位置参数(positional argument)调用,还能指定参数名,通过关键字参数(keyword argument)的方式调用。 比如下面这个用户查询函数: def que…

0x0000007b应用程序错误解决

系统win7 一 问题 今天部署应用程序到win7系统上,双击应用程序弹出了0x0000007b的错误。 二 vcredist 2.1 简介 一般用Visual C开发的Windows应用程序需要这个运行时库的支持才能在没有安装Visual C的计算机上正常运行,也可以在开发软件时选择”在静…

Python爬虫实战案例之——MySql数据入库

Hello大家好,我是你们的南枫学长,咱们今天来学——爬虫之MySql数据入库。 话不多说,导入咱们的老朋友: Pymysql就是我们Python里面的mysql库,主要功能就是用来连接MySql数据库,那么下载还是一样的操作去进…

自动驾驶规划-RTT* 算法 【免费获取Matlab代码】

目录 1.算法原理3.结果展示4.参考文献5.代码获取 1.算法原理 RRT(Rapidly-Exploring Random Trees) 快速随机扩展树,是一种单一查询路径规划算法。RRT 将根节点作为搜索的起点,然后通过随机撒点采样增加叶子节点的方式,生成一个随机扩展树&a…

STM32开发过程中碰到的问题总结 - 3

文章目录 前言1. keil5升级到最新版本使用armV6编译工具链编译不通过2. 最新的keil用Jlink调试失败3. 移动了目录后跑不起来了4. 串口兼容了GNU 和arm只会,编译出来的成果物,串口输出不正常5.STM32下哪些IO口可以作为中断触发去使用6. 触发GPIO10的外部中…

如何优化 Bash 脚本的执行效率?

要优化 Bash 脚本的执行效率,可以考虑以下几个方面: 减少命令执行次数:Bash 脚本中的命令执行是比较耗时的,在可能的情况下,可以尽量减少命令的执行次数。例如,可以将多个命令合并成一个,使用管…

【Go语言】面向对象编程(二):通过组合实现类的继承和方法重写

通过组合实现类的继承和方法重写 要实现面向对象的编程,就必须实现面向对象编程的三大特性:封装、继承和多态。 1 封装 类的定义及其内部数据的定义可以看作是类的属性,基于类定义的函数方法则是类的成员方法。 2 继承 Go 语言中&#x…