设计模式-02单例模式

单例模式

在有些系统中,为了节省内存资源、保证数据内容的一致性,对某些类要求只能创建一个实例,这就是所谓的单例模式。

定义

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

使用场景

  • 业务系统中全局只需要一个对象实例⽐如发号器(比如mysql的自增id)、redis连接对象(在配置中建立一个连接,不需要在使用的时候再建立连接,耗费性能)等
  • Spring IOC容器中的bean默认就是单例
  • Spring boot中controller,service,dao层中的@autowire的依赖注入对象都是单例

单例的分类(懒汉与饿汉)

  • 懒汉:懒加载,需要用到的时候创建对象,延迟加载
  • 饿汉:提前创建对象

实现步骤

  • 私有化构造函数(普通类创建对象,只需要new一下就行,单例不能把构造暴露出去)
  • 提供获取单例的方法(将构造好的对象暴露访问接口)

懒汉单例代码实现

第一种懒汉单例构造
public class LazySingleton {

    private static LazySingleton instance;

    //构造函数私有化
    private LazySingleton(){
        System.out.println("构造对象");
    }

    /**
     * @Description 对外暴露一个方法获取类的对象
     */
    public static LazySingleton getInstance(){
        if(instance == null){
            instance = new LazySingleton();
        }
        return instance;
    }
}

这种存在线程不安全问题,如果多线程情况下,线程A和线程B同时判断到instance为null,线程A和线程B都会创建一个对象,后一个创建的覆盖前一个,那这就不能保证是单例了。

第二种懒汉单例构造,方法加锁
        /**
     * 第二种方式加锁
     * 通过加锁 synchronized 保证单例
     * 采用synchronized对方法加锁 有很大的性能开销
     */
    public static synchronized LazySingleton getInstance(){
        if(instance == null){
            instance = new LazySingleton();
        }
        return instance;
    }

采用synchronized对方法加锁 有很大的性能开销,因此锁粒度改小

第三种实现方式 双重检查锁定DCL
/**
     * 第三种实现方式 双重检查锁定DCL (Double-CheckedLocking) ,在多线程情况下保持高性能
     *
     */
    public static  LazySingleton getInstance(){
        //多线程情况下,线程A和B都判断第一次instance == null,加锁只能保A和B不能同时创建对象。但是假如A拿到锁后创建对象后,B再拿到锁创建对象,B创建的对象覆盖A创建的,所以需要再一次判断instance == null。这就是DCL 
        if(instance == null){
            synchronized(LazySingleton.class){
                if(instance == null){
                    instance = new LazySingleton();
                }
            }
        }
        return instance;
    }

多线程情况下,线程A和B都判断第一次instance == null,加锁只能保A和B不能同时创建对象。但是假如A拿到锁后创建对象后,B再拿到锁创建对象,B创建的对象覆盖A创建的,所以需要再一次判断instance == null。这就是DCL

第三种方法依旧不安全,instance == new LazySingleton();不是原子操作。创建对象需要三个操作。

  • 1.分配空间给对象
  • 2.在空间内创建对象
  • 3.将对象赋值给引用instance

极端情况下,假如创建对象的步骤是1-》3-》2。执行3的时候,其他线程就会读取instance最新的值。但是这个时候对象是不安全的。(这个涉及指令重排的知识)

第四种 voliate 解决指令重排问题
    /**
     * 创建对象顺序
     * - 1.分配空间给对象
     * - 2.在空间内创建对象
     * - 3.将对象赋值给引用instance
     *
     * 极端情况下,假如创建对象的步骤是1-》3-》2。执行3的时候,其他线程就会读取instance最新的值。但是这个时候对象是不安全的。(这个涉及指令重排的知识)
     */
     
    private static volatile LazySingleton instance;
    
    /**
     * 第三种实现方式 双重检查锁定DCL (Double-CheckedLocking) ,在多线程情况下保持⾼高性能
     */
    public static  LazySingleton getInstance(){
        if(instance == null){
            synchronized(LazySingleton.class){
                if(instance == null){
                    instance = new LazySingleton();
                }
            }
        }
        return instance;
    }

饿汉单例代码实现

public class HungrySingleton {
    //在类加载的时候就创建了
    private static  HungrySingleton instance = new HungrySingleton();

    //构造方法私有化
    private HungrySingleton(){}

    //对外暴露一个方法获取类的对象
    public static  HungrySingleton getInstance(){
        return instance;
    }
}

懒汉饿汉选择

  • 懒汉代码实现麻烦,饿汉代码实现简单
  • 饿汉不存在多线程同步问题
  • 饿汉提前创建对象,会一直占着内存
  • 在对象不大,创建不复杂的情况下,直接使用饿汉单例

JDK源码中单例的应用

饿汉模式

Runtime runtime = Runtime.getRuntime();

懒汉模式

Console console = System.console();

序列化破坏单例

 LazySingleton instance = LazySingleton.getInstance();

        LazySingleton instance2 = null;
        //Write Obj to file
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("tempFile"));
        oos.writeObject(instance);
        //Read Obj from file
        File file = new File("tempFile");
        ObjectInputStream ois =  new ObjectInputStream(new FileInputStream(file));
        instance2 = (LazySingleton) ois.readObject();

        System.out.println(instance == instance2);
        

当单例implements Serializable,我们可以通过文件的反序列化获取对象,获取的对象.从上图的false说明经过I/O操作后,对象的状态发生了变化。也就是说对象在序列化与反序列化时出现了问题。

解决办法

    //在单例中加入
    private Object readResolve() {
        return instance;
    }

再跑一次结果

反射破坏单例

LazySingleton instance = LazySingleton.getInstance();
Constructor<?> constructor = LazySingleton.class.getDeclaredConstructor();
constructor.setAccessible(true);
LazySingleton instance2 = (LazySingleton) constructor.newInstance();
System.out.println(instance == instance2);

结果如下


构造方法执行了两遍。

解决方法

    private LazySingleton(){
        //解决反射导致的破坏单例
        if (instance!=null){
            throw new RuntimeException("单例不允许多实例");
        }
        System.out.println("构造对象");

    }

再一次执行结果如下


附源码:gitee源码地址

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值