Day43.单例模式 - 懒汉式、饿汉式、枚举式

本文详细介绍了Java中的单例模式,包括饿汉式、懒汉式(DCL懒汉式)、枚举实现及其优缺点。同时讨论了如何通过反射和序列化破坏单例,以及volatile关键字在防止指令重排中的作用。枚举实现被认为是线程安全、防止反射和序列化破坏的最佳单例模式实践。
摘要由CSDN通过智能技术生成

目录

关于单例模式(Singleton)

饿汉式(Hungry)

1、懒汉式(LazyMan)

2、DCL懒汉式 -- 双重检测锁模式

3、使用volatile防止指令重排

4、通过反射、序列化破坏单例模式

 枚举式(EnumSingle)


Java 中一般认为有 23 种设计模式,我们不需要所有的都会,但是其中常用的几种设计模式应该去掌握。总体来说设计模式分为三大类:

创建型模式,共五种:工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式。

结构型模式,共七种:适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式、享元模式。

行为型模式,共十一种:策略模式、模板方法模式、观察者模式、迭代子模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。

关于单例模式(Singleton)

单例模式(Singleton Pattern) - 保证一个类仅有一个对象,并提供一个它的全局访问点

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

常见应用场景:

  1. Windows的Task Manager(任务管理器)就是很典型的单例模式
  2. windows的Recycle Bin(回收站)也是典型的单例应用。在整个系统运行过程中,回收站一直维护着仅有的一个实例。
  3. 项目中,读取配置文件的类,一般也只有一个对象。没有必要每次使用配置文件数据,每次new一个对象去读取。
  4. 网站的计数器,一般也是采用单例模式实现,否则难以同步。
  5. 应用程序的日志应用,一般都何用单例模式实现,这一般是由于共享的日志文件一直处于打开状态,因为只能有一个实例去操作否则内容不好追加。
  6. 数据库连接池的设计一般也是采用单例模式,因为数据库连接是一种数据库资源。
  7. 操作系统的文件系统,也是大的单例模式实现的具体例子,一个操作系统只能有一个文件系统。
  8. Application 也是单例的典型应用(Servlet编程中会涉及到)
  9. 在Spring中,每个Bean默认就是单例的,这样做的优点是Spring容器可以管理
  10. 在servlet编程中,每个Servlet也是单例
  11. 在spring MVC框架/struts1框架中,控制器对象也是单例
  12. 一个产品注册了一个商标,那么它就是单例的

单例模式的优点:

  1. 由于单例模式只生成一个实例,减少了系统性能开销,当一个对象的产生需要比较多的资源时,如读取配置、产生其他依赖对象时,则可以通过在应用启动 时直接产生一个单例对象,然后永久驻留内存的方式来解决
  2. 单例模式可以在系统设置全局的访问点,优化环共享资源访问,例如可以设计 一个单例类,负责所有数据表的映射处理
     

单例模式的类型(待补充):

  • 饿汉式:在类加载时已经创建好该单例对象,等待被程序使用
  • 懒汉式:在真正需要使用对象时才去创建该单例类对象
  • 枚举式:枚举本身就是单例模式,线程安全,实现简单,调用效率高,不能延时加载。

一、饿汉式(Hungry)

饿汉式在类加载时已经创建好该对象,在程序调用时直接返回该单例对象即可,即我们在编码时就已经指明了要马上创建这个对象,不需要等到被调用时再去创

public class hungry {
    // jvm保证在任何线程访问uniqueInstance静态变量之前一定先创建了此实例 
    private final static hungry HUNGRY = new hungry();

    // 私有构造器,外界无法实例化
    private hungry(){}

    // 提供全局访问点获取唯一的实例
    public hungry getHungry(){
        return HUNGRY;
    }
}
  • 存在的问题:可能会浪费内存空间

二、懒汉式(LazyMan)

懒汉式--在使用时候才去实例化单例对象

public class lazyMan {
    private lazyMan(){}
    private static lazyMan LazyMan;
    public static lazyMan getLazyMan(){
        //对象唯一
        if(LazyMan==null){    
            LazyMan = new lazyMan();
        }
        return LazyMan;
    }
}
  • 安全问题 

 但由于创建对象并非原子性操作,多线程并发可能会创建多个对象,我们需要进一步优化

2、DCL懒汉式 -- 双重检测锁模式

  • DCL:Double Check(双重校验) + Lock(加锁)
    public static lazyMan getLazyMan(){
    //多线程同时看到LazyMan==null,如果不为null,则直接返回LazyMan
        if(LazyMan==null){
            synchronized (lazyMan.class){
                if(LazyMan==null){
                    //其中一个线程进入,另一个则检测已不为null
                    LazyMan = new lazyMan();    //不是原子性操作
                    /**
                     * 1、分配内存空间
                     * 2、执行方法构造,初始化内存
                     * 3、把这个对象指向这个空间
                     * 1,3,2  A
                     * 若此时 B 进入,LazyMan还没完成构造,代码仍存在问题
                     */
                }
            }
        }
        return LazyMan;
    }

但由于创建对象并非原子性操作,以上代码还存在问题,指令重排

3、使用volatile防止指令重排

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

创建对象时,在JVM会经过三步:

(1)分配内存空间
(2)执行方法构造,初始化内存
(3)将对象指向分配好的这个空间

如果A线程创建对象的步骤为1、3、2,B线程在执行完1、3后进入,判定到LazyMan已不为空,return 获取到未初始化的LazyMan对象,则会造成空指针异常。

  • volatile

使用volatile关键字修饰的变量,可以保证其指令执行的顺序与程序指明的顺序一致,不会发生顺序变换。

volatile还有第二个作用:使用volatile关键字修饰的变量,可以保证其内存可见性,即每一时刻线程读取到该变量的值都是内存中最新的那个值,线程每次操作该变量都需要先读取该变量。

  • 最终代码:
public class lazyMan {

    private lazyMan(){}

    private static volatile lazyMan LazyMan; //volatile防止指令重排

    public static lazyMan getLazyMan(){
        if(LazyMan==null){
            synchronized (lazyMan.class){
                if(LazyMan==null){
                    LazyMan = new lazyMan();
                }
            }
        }
        return LazyMan;
    }
}

4、通过反射、序列化破坏单例模式

但由于反射和序列化的存在,他们依然可以将单例对象破坏(产生多个对象),造成安全问题。

1:演示利用反射破坏单例模式

public static void main(String[] args) throws Exception {
    // 获取类的显式构造器
    Constructor<lazyMan> lazyManConstructor = lazyMan.class.getDeclaredConstructor();
    // 可访问私有构造器
    lazyManConstructor.setAccessible(true);
    // 利用反射构造新对象
    lazyMan lazyMan = lazyManConstructor.newInstance();
    lazyMan lazyMan2 =lazyManConstructor.newInstance();
    System.out.println(lazyMan == lazyMan2); //false
}

2:利用序列化与反序列化破坏单例模式

public static void main(String[] args) {
    // 创建输出流
    ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("lazyMan.file"));
    // 将单例对象写到文件中
    oos.writeObject(Singleton.getInstance());
    // 从文件中读取单例对象
    File file = new File("lazyMan.file");
    ObjectInputStream ois =  new ObjectInputStream(new FileInputStream(file));
    lazyMan newInstance = (lazyMan) ois.readObject();
    // 判断是否是同一个对象
    System.out.println(newInstance == lazyMan.getInstance()); // false
}

三、枚举式(EnumSingle)

在 JDK1.5 后,使用 Java 语言实现单例模式的方式又多了一种:枚举

// enum 本身也是一个Class类
public enum EnumSingle {
    //定义一个枚举的元素,它就代表了Singleton的一个实例。
    INSTANCE;

    //对外部提供调用方法:将创建的对象返回,只能通过类来调用
    public void otherMethod(){
        //功能处理
    }
    public static void main(String[] args) {
        EnumSingle i1 = EnumSingle.INSTANCE;
        EnumSingle i2 = EnumSingle.INSTANCE;
        System.out.println(i1==i2); //true
    }
}

(1)防反射:在利用反射调用 newInstance() 时,会判断该类是否是一个枚举类,如果是,则抛出异常。newInstance()源码:

  •  Cannot reflectively create enum objects  - 不能反射地创建enum对象

(2)防止反序列化:  在读入Singleton对象时,每个枚举类型和枚举名字都是唯一的,所以在序列化时,仅仅只是对枚举的类型和变量名输出到文件中,在读入文件反序列化成对象时,利用 Enum 类的 valueOf(String name) 方法根据变量的名字查找对应的枚举对象。

所以,在序列化和反序列化的过程中,只是写出和读入了枚举类型和名字,没有任何关于对象的操作。

小总结:

(1)Enum 类内部使用Enum 类型判定防止通过反射创建多个对象

(2)Enum 类通过写出(读入)对象类型和枚举名字将对象序列化(反序列化),通过 valueOf() 方法匹配枚举名找到内存中的唯一的对象实例,防止通过反序列化构造多个对象

(3)枚举类不需要关注线程安全、破坏单例和性能问题,因为其创建对象的时机与饿汉式单例有异曲同工之妙

————————————————
参考:https://blog.csdn.net/weixin_41949328/article/details/107296517 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值