波吉学设计模式——玩转单例模式

Posted 一直会努力的波吉

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了波吉学设计模式——玩转单例模式相关的知识,希望对你有一定的参考价值。

单例模式作为设计模式中最常见最重要的设计模式,今天波吉带你由浅入深的明白单例模式,相信你一定会有所收获的

单例模式

简介

​ 所谓类的单例设计模式,就是采取一定的方法保证在整个的软件系统中,对某个类只能存在一个对象实例,并且该类只提供了一个取得其对象实例的方法。如果我们要让类在一个虚拟机中只能产生一个对象,我们首先必须将类的构造器的方法权限设置为private,这样就不能用new操作符在类的外部产生类的对象了,但是类内部仍然可以产生该类的对象,因为在类的外部开始还没发得到类的对象,只能调用该类的某个静态方法以返回类内部构建的对象,静态方法只能访问类中的静态成员变量,所以指向类内部产生的该类对象的变量也必须定义为静态的

核心作用

保证一个类只有一个对象,并且提供一个访问该实例的全局访问点

优点

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

应用场景

​ 单例模式只生成了一个实例,减少了系统性能开销,当一个对象的产生需要比较多的资源时,如读取配置,产生其它依赖对象,则可以通过在应用启动时直接产生一个单例对象,然后永久驻留内存的方式来解决,例如:java.lang.Runtime,网站计数器,应用程序的日志应用,数据库连接池,读取配置文件的类,Application,Windows中的任务管理器和回收站

  • 业务系统全局只需要一个对象实例,比如发号器,redis连接对象等
  • Spring IOC容器中的Bean默认就是单例
  • Spring Boot中的Controller,Service,Dao层中 通过 @AutoWrie的依赖注入默认就是单例的

主要分类

  • 饿汉:就是所谓的懒加载,延迟创建对象,需要使用的时候再延时创建对象,因为饿汉单例即使没有调用方法也会占据内存所以我们一般不采用
  • 懒汉:懒汉由于调用实例方法对象才会生成所以更符合我们的需求

饿汉单例

线程安全

class Bank 
    //1.私有化构造器,不允许外部可以调用
    private Bank() 

    

    //2.内部创建类的对象
    //4.要求此对象必须声明为静态
    private static Bank instance = new Bank();

    //3.提供公共的方法,返回类的对象
    public static Bank getInstance() 
        return instance;
    


public class SingletonTest1 
    public static void main(String[] args) 
        Bank bank = Bank.getInstance();
    


饿汉模式简单了解后我们着重分析懒汉单例

单例模式实现步骤:

  • 私有化构造函数
  • 提供获取单例的⽅法

懒汉单例

这是最简单实现懒汉单例的实例,后续内容会对懒汉单例做出升级!在本次实例中线程不安全

/**
 * 单例设计模式——懒汉
 *
 * @author ccy
 * @version 1.0
 * @date 2021/12/6 13:18
 */
class Order 
    //1.私有化构造器,不允许外部可以调用
    private Order() 

    

    //2.声明当前类的对象
    //4.要求此对象必须声明为静态
    private static Order instance = null;

    //3.提供公共的方法,返回类的对象
    public static Order getInstance() 
        if (instance == null) 
            instance = new Order();
        
        return instance;
    


标题写到这种情况的单例模式是线程不安全原因就在于在高并发的场景下会创建多个对象违背了单例模式只创建一次的情况

为了应对高并发下能够只创建一次对象的情况所以我们引入Synchroized,但是采用synchronized 对方法加锁有很大的性能开销,因为当getInstance()内部逻辑比较复杂的时候,在高并发条件下没获取到加锁方法执行权的线程,都得等到这个方法内的复杂逻辑执行完后才能执行,等待浪费时间,效率比较低

这种实现懒汉单例线程是安全的但是不采用它的原因就在于synchronized带来的效率低

class Order 
    //1.私有化构造器,不允许外部可以调用
    private Order() 

    

    //2.声明当前类的对象
    //4.要求此对象必须声明为静态
    private static Order instance = null;

    //3.提供公共的方法,返回类的对象
    public synchronized static Order getInstance() 
        if (instance == null) 
            instance = new Order();
        
        return instance;
    

为了满足以上需求,DCL双重检测锁机制的单例模式就出现了

双重检测锁模式

下面是上述代码的运行顺序:

  1. 检测实例是否已经初始化创建,如果是则立即返回
  2. 获得锁
  3. 再次检测实例是否已经初始化创建成功,如果还没有则创建实例
/**
 * 单例设计模式——懒汉
 *
 * @author ccy
 * @version 1.0
 * @date 2021/12/6 13:18
 */
class Order 
    //1.私有化构造器,不允许外部可以调用
    private Order() 

    

    //2.声明当前类的对象
    //4.要求此对象必须声明为静态
    private static Order instance = null;

    //3.提供公共的方法,返回类的对象
    public static Order getInstance() 
        if (instance == null) 
            synchronized (Order.class) 
                if (instance == null) 
                    instance = new Order();
                
            
        
        return instance;
    


public class SingletonTest2 
    public static void main(String[] args) 
        Order order1 = Order.getInstance();
        Order order2 = Order.getInstance();
        System.out.println(order1 == order2);
    

DCL双重检测锁机制在逻辑上的确是趋近于完美了但是!!!由于指令重排的原因仍然有可能会创建多个对象,因为instance = new Order()这行代码的执行逻辑是

  1. 在堆中开辟对象所需空间,分配地址
  2. 根据类加载的初始化顺序进行初始化
  3. 将内存地址返回给栈中的引用变量

由于指令重排(你可以把它理解成编译器为了优化代码而对实际写出的代码在机器中进行重新排序导致原本的代码执行顺序发生变化)的缘故会出现这样的情况

  1. 在堆中开辟对象所需空间,分配地址
  2. 将内存地址返回给栈中的引用变量(此时变量已不为null,但是变量却并没有初始化完成)
  3. 根据类加载的初始化顺序进行初始化

在多线程下指令重排给带来的问题就会被放大

执行顺序Thread1Thread2
1第一次检测, instance 为null
2获取锁
3第二次检测, instance 为null
4在堆中分配内存空间
5instance 指向分配的内存空间
6第一次检测,instance不为null
7此时Thread 2对 instance的访问,访问到的是一个还未完成初始化的对象。所以在使用 instance 时可能会出错
8初始化 instance

所以为了避免指令重排我们只需要对初始化对象加volatile这一关键字即可,volatile是可以预防指令重排

下面代码是正确的双重检测锁机制

/**
 * 正确的双重检测锁机制
 *
 * @author ccy
 * @version 1.0
 * @date 2021/12/6 13:18
 */
class Order 
    //1.私有化构造器,不允许外部可以调用
    private Order() 

    

    //2.声明当前类的对象
    //4.要求此对象必须声明为静态
    private volatile static Order instance = null; // 注意哦这里添加了关键字volatile为了防止指令重排

    public static Order getInstance() 
        if (instance == null) 
            synchronized (Order.class) 
                if (instance == null) 
                    instance = new Order();
                
            
        
        return instance;
    

虽然这种单例模式线程安全虽然构造方法是私有的但是Java中反射可以破坏私有方法,我们仍然可以通过反射来获取对象

反射破坏双重检测锁机制

public class Lazy 
    private Lazy() 
        System.out.println(Thread.currentThread().getName());
    
    private volatile static Lazy lazy;

    public static Lazy getInstance() 
        if(lazy == null) 
            synchronized (Lazy.class) 
                if(lazy == null) 
                    lazy = new Lazy();
                
            
        
        return lazy;
    

    public static void main(String[] args) throws Exception 
        Lazy instance1 = Lazy.getInstance();
        Constructor<Lazy> declaredConstructor = Lazy.class.getDeclaredConstructor(null);
        declaredConstructor.setAccessible(true);
        Lazy instance2 = declaredConstructor.newInstance();
        System.out.println(instance1 == instance2);
    

枚举

没办法了只能搬出杀手锏了枚举元素天生就是线程安全的单例,调用效率也高,只是无法延时加载!

main方法中是我尝试使用反射来破坏枚举单例

public enum EnumSingle 

    INSTANCE;

    public static EnumSingle getInstance() 
        return INSTANCE;
    

//尝试使用反射破坏枚举单例
class TestSingle 
    public static void main(String[] args) throws Exception 
        EnumSingle instance = EnumSingle.getInstance();
//        EnumSingle instance1 = EnumSingle.INSTANCE;
//        System.out.println(instance == instance1);
        Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor();
        declaredConstructor.setAccessible(true);
        EnumSingle instance2 = declaredConstructor.newInstance();
        System.out.println(instance == instance2);
    

虽然IDEA报错了但是错误并不是我们预计的错误

但从.class文件来看确实是存在无参构造的方法经过一系列手段后我们了解到实际上调用的是有参的构造方法

Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor(String.class,int.class);调整代码以后报错成了我们预计的


单例模式我们今天就学到这里吧,如果博文对你有帮助

以上是关于波吉学设计模式——玩转单例模式的主要内容,如果未能解决你的问题,请参考以下文章

彻底玩转单例模式

彻底玩转单例模式

JUC并发编程(13)--- 彻底玩转单例模式

15彻底玩转单例模式

15彻底玩转单例模式

单例模式_反射破坏单例模式_枚举类_枚举类实现单例_枚举类解决单例模式破坏