• 23种设计模式——单例模式


    单例模式

    单例模式概述

    单例模式是指在内存中只会创建且仅创建一次对象的设计模式。在程序中多次使用同一个对象且作用相同时,为了防止频繁地创建对象使得内存飙升,单例模式可以让程序仅在内存中创建一个对象,让所有需要调用的地方都共享这一单例对象。

    1. 单例类只能有一个实例
    2. 单例类必须自己创建自己的唯一实例
    3. 单例类必须给所有其他对象提供这一实例

    在这里插入图片描述

    单例模式类型

    首先看一下原型模式:

    @Data
    public class Single {
        private String singleName;
    
    
        public static void main(String[] args) {
            Single single1 = new Single();
            Single single2 = new Single();
            single1.setSingleName("");
            System.out.println(single1.singleName);
            System.out.println(single2.singleName);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    测试结果:

    single1
    null

    懒汉式

    真正需要使用对象时才去创建该单例类对象

    /**
     * 懒汉式单例
     */
    @Data
    public class LazySingle {
        private String singleName;
        private static LazySingle single=null;
    
        public static LazySingle getInstance(){
            if(single==null){
                single=new LazySingle();
            }
            return single;
        }
    
        public static void main(String[] args) {
            LazySingle single1=LazySingle.getInstance();
            LazySingle single2=LazySingle.getInstance();
            single1.setSingleName("懒汉式");
            System.out.println(single1.singleName);
            System.out.println(single2.singleName);
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    测试结果

    懒汉式
    懒汉式

    解决懒汉式的线程安全问题

    由代码可以看出,线程是不安全的,多线程情况下不能保证是单例的,解决方案肯定是加锁,但加锁会导致性能低下,所以解决方案应该兼顾性能和安全实现

    解决方案为: Double Check(双重校验) + Lock(加锁)

    public static LazySingle getInstance() {
        if (singleton == null) {  // 线程A和线程B同时看到singleton = null,如果不为null,则直接返回singleton
            synchronized(LazySingle.class) { // 线程A或线程B获得该锁进行初始化
                if (singleton == null) { // 其中一个线程进入该分支,另外一个线程则不会进入该分支
                    singleton = new LazySingle();
                }
            }
        }
        return singleton;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    但在JVM运行过程中会有一个问题:

    指令重排

    JVM在保证最终结果正确的情况下,可以不按照程序编码的顺序执行语句,尽可能提高程序的性能

    JVM创建一个对象会经过3步:

    1. 为对象分配内存空间
    2. 初始化对象
    3. 将对象指向分配好的内存空间

    在这三步中,第2、3步有可能会发生指令重排现象,创建对象的顺序变为1-3-2,会导致多个线程获取对象时,有可能线程A创建对象的过程中,执行了1、3步骤,线程B判断singleton已经不为空,获取到未初始化的singleton对象,就会报NPE异常。

    解决方案:使用volatile关键字修饰

    1. 可以保证其指令执行的顺序与代码顺序一致,不会发生顺序变换

    2. 可以保证其内存可见性,即每一时刻线程读取到该变量的值都是内存中最新的那个值,线程每次操作该变量都需要先读取该变量。

    最终解决方案如下:

    /**
     * 懒汉式单例
     */
    @Data
    public class LazySingle {
        private String singleName;
        private  static volatile LazySingle single=null;
    
        public static LazySingle getInstance(){
            if(single==null){
                synchronized (LazySingle.class){
                    if(single == null){
                        single=new LazySingle();
                    }
                }
            }
            return single;
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    饿汉式

    饿汉式在类创建的同时就已经创建好一个静态的对象供系统使用,以后不再改变,所以天生是线程安全的

    /**
     * 饿汉式单例
     */
    @Data
    public class HungrySingle {
        private String singleName;
        private  static final HungrySingle singleton=new HungrySingle();
        public static HungrySingle getInstance() {
            return singleton;
        }
    
        public static void main(String[] args) {
            HungrySingle single1 = HungrySingle.getInstance();
            HungrySingle single2 = HungrySingle.getInstance();
            single1.setSingleName("饿汉式");
            System.out.println(single1.singleName);
            System.out.println(single2.singleName);
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    测试结果

    饿汉式
    饿汉式

    破坏单例的情况

    java的反射和序列化可以破坏单例模式(饿汉式和懒汉式)

    1. 使用反射破坏单例模式(演示饿汉式)
     try {
                //获取类的显式构造器
                Constructor<HungrySingle> constructor = HungrySingle.class.getDeclaredConstructor();
                // 可访问私有构造器
                constructor.setAccessible(true);
                HungrySingle singleton1 = constructor.newInstance();
                HungrySingle singleton2 = constructor.newInstance();
                System.out.println(singleton1==singleton2);   //false
            } catch (Exception e) {
                e.printStackTrace();
            }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    1. 使用序列化与反序列化破坏单例模式(演示懒汉式)
     try {
                File file = new File("Singleton.txt");
                //创建输出流
                ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(file));
                //将单例对象写到文件中  序列化
                oos.writeObject(LazySingle.getInstance());
                //从文件读取单例对象
                ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
                //反序列化得到对象lazySingle
                LazySingle lazySingle=(LazySingle)ois.readObject();
                System.out.println(lazySingle==LazySingle.getInstance()); //false
            } catch (Exception e) {
                e.printStackTrace();
            }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    枚举实现单例模式

    public enum Sex {
        MALE,FEMALE;
    
    
        public static void main(String[] args) {
            Sex male1 = Sex.MALE;
            Sex male2 = Sex.MALE;
            System.out.println(male1==male2);//true
    
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    枚举的优势:

    1. 代码简洁
    2. 不需要额外操作保证线程安全和对象单一性
    3. 反射、序列化不能破坏枚举类的单例模式
      • 枚举类默认继承了 Enum 类,在利用反射调用 newInstance() 时,会判断该类是否是一个枚举类,如果是,则抛出异常。
      • 在序列化和反序列化的过程中,只是写出和读入了枚举类型和名字,没有任何关于对象的操作。

    总结

    1. 单例模式常见实现方式:饿汉式懒汉式

    2. 懒汉式:在需要用到对象时才实例化对象,正确的实现方式是:Double Check + Lock,解决了并发安全和性能低下问题

    3. 饿汉式:在类加载时已经创建好该单例对象,在获取单例对象时直接返回对象即可,不会存在并发安全和性能问题。

    4. 懒汉式与饿汉式的选择:

      • 内存要求非常高,使用懒汉式写法,可以在特定时候才创建该对象;
      • 内存要求不高,使用饿汉式写法,因为简单不易出错,且没有任何线程安全和性能问题
    5. 最佳实现方式:枚举, 其代码精简,没有线程安全问题,且 Enum 类内部防止反射和反序列化时破坏单例

  • 相关阅读:
    Haskell网络编程:从数据采集到图片分析
    MySQL的索引和事务
    C/C++基础 异常处理关键字 throw、try、catch
    【Spring-3】ConfigurationClassPostProcessor解析配置类
    Redis过期
    RocketMq源码分析(二)--nameServer启动流程
    Python轻量级Web框架:Bottle库!
    【陕西理工大学-数学软件实训】数学实验报告(8)(数值微积分与方程数值求解)
    文件上传漏洞详解
    LeetCode刷题---合并两个有序链表
  • 原文地址:https://blog.csdn.net/qq_45925787/article/details/126347682