手撕设计模式——计划生育之单例模式

1.业务需求

​ 大家好,我是菠菜啊。80、90后还记得计划生育 这个国策吗?估计同龄的小伙伴们,小时候常常被"只生一个好""少生、优生"等宣传标语洗脑,如今国家已经放开并鼓励生育了。话说回来,现实生活中有计划生育,你知道设计模式中也有计划生育吗?它是怎么实现的?

2.代码实现

我们只要保证一个类只有一个实例化对象,这样就能达到计划生育的目的。其实这个设计模式大家应该都很熟悉了,叫做单例模式

实现思路

类的实例化交给类本身,对外提供一个访问该单例的全局访问点,重点考虑线程安全、系统资源消耗、反射以及反序列化破坏等因素。

2.1 静态代码块

java 复制代码
public  class StaticBlockSingleton {

    private static StaticBlockSingleton singleton;

    static {
        singleton=new StaticBlockSingleton();
    }

    private StaticBlockSingleton(){

    }

    public static StaticBlockSingleton getInstance(){
        return singleton;
    }
}

2.2 饿汉式

java 复制代码
public  class HungrySingleton  {

    private static HungrySingleton singleton=new HungrySingleton();

    private HungrySingleton(){

    }

    public static HungrySingleton getInstance(){
        return singleton;
    }
}

思考 :静态代码块和饿汉式不管singleton对象有没有被使用,都会在系统初始化的时候初始化对象从而占用系统资源

2.3 懒汉式

java 复制代码
public  class LazySingleton implements Serializable {

    //volatile 防止指令重排
    private static volatile LazySingleton singleton;

    private LazySingleton(){

    }

    public static LazySingleton getInstance(){
        //1.第一层检索 提高执行效率 如果不是null 直接返回
        if(null==singleton){
            //2.多个线程同时进入 获取锁的执行,没有获取锁的等待
            synchronized (LazySingleton.class){
                //3.防止2步骤有等待锁的线程 锁释放后拿到锁后需判断一下对象是否创建 
                if (null==singleton){
                    singleton=new LazySingleton();
                }
            }
        }
        return singleton;
    }
}

思考 :**双重检查锁定(Double-Check Locking)**懒汉式让对象实例化延迟加载,减少了对象未被使用而占用系统资源,但是引入了锁,系统性能有一定影响。volatile关键字会屏蔽Java虚拟机所做的一些代码优化,也可能会导致系统运行效率降低。

拓展:指令重排

问题:

​ 为什么DCL实现单例,还需要用volatile修饰实例呢?

分析:

​ 问题出现在'singleton=new LazySingleton();'这行代码,java创建对象不是一个原子操作,可以被分解为3步:

java 复制代码
//1.分配对象的内存空间
//2.初始化对象
//3.将instance指向刚分配的内存地址

编译器或者处理器在执行代码的时候为了最大地提高性能,可能会将执行执行顺序重排,2和3执行顺序可能是相反的。在单线程情况下,重排序没有什么问题,因为他最终结果都是一致的。但是如果在多线程并发下,就会有可能有问题了。下面模拟俩个线程创建单例的场景:

CPU时间片 线程A 线程B
T1 A-1:分配singleton对象的内存空间
T2 A-3:将instance指向刚分配的内存地址 B-1:第一层判断instance是否为null
T3 B-2:instance不为null,B线程获得instance引用的对象
T4 A-2:初始化对象
T5 A-4:A线程获得instance引用的对象

​ 如果按照上面的顺序,B线程获取到的是一个未初始化的对象,这就有问题了。解决方案就是引入volatile关键字,它有俩个作用:一是保证变量的内存可见性,二是禁止指令重排。

保证变量内存可见性:

如果属性被volatile修饰,相当于会告诉CPU,对当前属性的操作,不允许使用CPU的缓存,必须去和主内存操作。

volatile的内存语义:

  • volatile属性被写:当写一个volatile变量,JMM会将当前线程对应的CPU缓存及时的刷新到主内存中
  • volatile属性被读:当读一个volatile变量,JMM会将对应的CPU缓存中的内存设置为无效,必须去主内存中重新读取共享变量

禁止指令重排:

当我们使用 volatile 关键字来修饰一个变量时,Java 内存模型会插入内存屏障(一个处理器指令,可以对 CPU 或编译器重排序做出约束)来确保以下两点:

  • 写屏障(Write Barrier):当一个 volatile 变量被写入时,写屏障确保在该屏障之前的所有变量的写入操作都提交到主内存。
  • 读屏障(Read Barrier):当读取一个 volatile 变量时,读屏障确保在该屏障之后的所有读操作都从主内存中读取。

2.4 静态内部类

java 复制代码
public class StaticInnerSingleton implements Serializable {

    private static class InnerSingleton{
        private static final StaticInnerSingleton instance = new StaticInnerSingleton();
    }

    private StaticInnerSingleton(){

    }
    public static StaticInnerSingleton getInstance(){
        return InnerSingleton.instance;
    }
    
}

思考 :静态instance不是StaticInnerSingleton类的成员变量,所以在类加载的时候不会实例化instance,当第一次调用getInstance方法时,内部类InnerSingleton类会初始化instance,JVM保证其线程安全性,确保该成员变量只初始化一次。既实现了延迟加载,又没有性能消耗所以静态内部类这种方式比较推荐。但是有反射和反序列化破坏问题

2.5 枚举

java 复制代码
public enum  Singleton implements Serializable {
    INSTANCE;
    
    void doSomething(){
        System.out.println("do something");
    }

}

思考:枚举可以天然的防止反射和反序列化,但是不能延迟加载,这种方式是《Effective Java》作者的Josh Bloch提倡的方式。

拓展:反射和反序列化破坏单例

反射破坏单例

破坏案例:

以DCL为例,反射生成俩个对象。

java 复制代码
public class Client {
    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        //获取类的构造器
        Constructor<LazySingleton> constructor = LazySingleton.class.getDeclaredConstructor();
        //设置权限
        constructor.setAccessible(true);
        //使用 constructor 创造对象
        LazySingleton obj1 = constructor.newInstance();
        LazySingleton obj2 = constructor.newInstance();
        System.out.println(obj1);
        System.out.println(obj2);

    }
}

运行结果

打印俩次对象的地址不一样,说明俩个对象不是同一个。

预防措施:

可以在构造方法中抛出异常

java 复制代码
public  class LazySingleton implements Serializable {

    //volatile 防止指令重排
    private static volatile LazySingleton singleton;

    private LazySingleton(){
        if(singleton!=null){
            throw new RuntimeException("不允许重复创建对象!");

        }
    }
 }

枚举预防源码:

newInstance方法单独判断是否是枚举类型,如果是的话抛出异常,防止反射破坏单例模式。


*

反序列化破坏单例

破坏案例:

以DCL为例,反序列化生成俩个对象。

java 复制代码
public class Client2 {
    public static void main(String[] args) throws Exception {
        LazySingleton hungrySingleton = LazySingleton.getInstance();
        System.out.println(hungrySingleton);

        //将得到的实例序列化到磁盘
        FileOutputStream fileOutputStream = new FileOutputStream("D:/LazySingleton.txt");
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
        objectOutputStream.writeObject(hungrySingleton);
        objectOutputStream.flush();
        objectOutputStream.close();

        //从磁盘反序列化得到实例
        FileInputStream fileInputStream = new FileInputStream("D:/LazySingleton.txt");
        ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
        LazySingleton singleton = (LazySingleton) objectInputStream.readObject();
        System.out.println(singleton);

    }
}

运行结果

打印俩次对象的地址不一样,说明俩个对象不是同一个。

原因分析

obj = desc.isInstantiable() ? desc.newInstance() : null; 这行代码的意思是:如果这个类可以序列化,就创建新对象,不行就返回null。

预防措施

添加readResolve(),返回单例对象。当反序列化恢复一个新对象时,系统会自动调用这个readResolve()方法返回指定好的对象。

java 复制代码
public  class LazySingleton implements Serializable {

    //volatile 防止指令重排
    private static volatile LazySingleton singleton;


    private Object readResolve(){
        return singleton;
    }
}

枚举预防源码:

readEnum方法中'Enum<?> en = Enum.valueOf((Class)cl, name);'这行代码,等提供于将instance对象赋值给en,枚举做了特殊处理,防止反序列化破坏单例模式。

3.定义以及实现步骤

单例模式(Singleton)指一个类只有一个实例,且该类能自行创建这个实例的一种模式。

​ 通用实现步骤:

  1. 私有化构造方法
  2. 在单例内部创建一个唯一实例
  3. 提供一个外部获取实例的方法

4.优缺点以及应用场景

优点:

  • 提供了唯一实例的全局访问方法,可以优化共享资源的访问
  • 避免对象的频繁创建和销毁,可以提高性能

缺点:

  • 单例模式的代码基本上在一个类中,违反了单一职责
  • 单例模式不易扩展,扩展需要修改原来的代码,违背开闭原则

适用场景:

  • 创建一个对象资源消耗过高,并且只需一个
  • 只允许使用一个公共访问点

现实应用场景:

  • 数据库连接池
  • 手机app窗口(大多数app)
  • Spring中Bean的默认生命周期
  • Windows任务管理器

你的收藏和点赞就是我最大的创作动力,关注我我会持续输出更新!

友情提示:请尊重作者劳动成果,如需转载本博客文章请注明出处!谢谢合作!

【作者:我爱吃菠菜 】

相关推荐
挺菜的13 分钟前
【算法刷题记录(简单题)003】统计大写字母个数(java代码实现)
java·数据结构·算法
掘金-我是哪吒1 小时前
分布式微服务系统架构第156集:JavaPlus技术文档平台日更-Java线程池使用指南
java·分布式·微服务·云原生·架构
亲爱的非洲野猪1 小时前
Kafka消息积压的多维度解决方案:超越简单扩容的完整策略
java·分布式·中间件·kafka
wfsm1 小时前
spring事件使用
java·后端·spring
微风粼粼2 小时前
程序员在线接单
java·jvm·后端·python·eclipse·tomcat·dubbo
缘来是庄2 小时前
设计模式之中介者模式
java·设计模式·中介者模式
rebel2 小时前
若依框架整合 CXF 实现 WebService 改造流程(后端)
java·后端
代码的余温3 小时前
5种高效解决Maven依赖冲突的方法
java·maven
慕y2743 小时前
Java学习第十六部分——JUnit框架
java·开发语言·学习
paishishaba4 小时前
Maven
java·maven