单例设计模式你真的会用吗?

Posted 91sai

tags:

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

1 快速入门

应用场景:一些重量级的对象(如:数据库连接池)不需要多个实例

实现方式如下:

  • 懒汉模式

    需要用到时采取创建实例

  • 饿汉模式

    类加载的初始化阶段去创建

  • 静态内部类

    类加载的初始化阶段去创建+懒汉模式

1.1 懒汉模式

示例代码如下

import lombok.Data;

/**
 * 懒汉模式实现单例
 * @author sai
 * @version 1.0
 * @date 2022/3/27 14:55
 */
@Data
public class LazySingleModel 
    private Integer param1 = 1;
    private String param2 = "1";

    /**
     * 使用 volatile 禁止指令重排,防止并发场景下出现空指针错误
     */
    private volatile static LazySingleModel instance;

    private LazySingleModel() 

    /**
     * 获取实例方法
     */
    public static LazySingleModel getInstance() 
        if (null == instance) 
            synchronized (LazySingleModel.class) 
                // 使用 double check 防止多线程造成重复创建多个实例
                if (null == instance) 
                    instance = new LazySingleModel();
                    return instance;
                
            
        
        return instance;
    

    public static void main(String[] args) 
        LazySingleModel singleModel = LazySingleModel.getInstance();
        // 如果不加 volatile,在高并发场景下,下面这行代码可能会出现空指针异常
        String param1 = singleModel.getParam1().toString();
        System.out.println(param1 + singleModel.getParam2());
    

优点:

  • 用到时才去加载

缺点:

  • 第一次去创建单例时遇到了并发较高的话,性能较差
  • 要自己保证线程安全

1.2 饿汉模式

示例代码如下

/**
 * 饿汉模式
 * @author sai
 * @version 1.0
 * @date 2022/3/27 15:07
 */
public class HungerSingleModel 
    private static final HungerSingleModel INSTANCE = new HungerSingleModel();

    private HungerSingleModel() 

    public static HungerSingleModel getInstance() 
        return INSTANCE;
    

    public static void main(String[] args) 
        HungerSingleModel instance1 = HungerSingleModel.getInstance();
        HungerSingleModel instance2 = HungerSingleModel.getInstance();
        System.out.println(instance1 == instance2);
    

优点:

  • 类加载完成时就已经创建好单例对象了,就算遇到高并发请求也没事
  • 实现简单

缺点:

  • 有时我们 JVM 加载这个类时,不一定是要用到实例对象的,那么创建单例和类加载强捆绑一起也不是那么的合适

1.3 静态内部类实现

示例代码如下

/**
 * 静态内部类
 * @author sai
 * @version 1.0
 * @date 2022/3/27 15:23
 */
public class InnerClassSingleModel 

    private InnerClassSingleModel() 
    

    static class InnerClassSingleModelHandle 
        private static final InnerClassSingleModel INSTANCE = new InnerClassSingleModel();
    

    public static InnerClassSingleModel getInstance() 
        return InnerClassSingleModelHandle.INSTANCE;
    

    public static void main(String[] args) 
        // 懒加载:第一次调用获取单例方法时才会去加载 InnerClassSingleModelHandle,这时才创建 InnerClassSingleModel 的实例对象
        // ps:JVM 类加载机制也是采用懒加载方式的
        InnerClassSingleModel instance = InnerClassSingleModel.getInstance();
        System.out.println(instance);
    

就一般而言,我们写单例时比较推荐这种实现方式,因为它具有懒汉模式和饥汉模式的优点。

2 上面所有的示例代码居然是有问题的!?

下面均用静态内部类的示例代码为例子说明问题所在,以及如何修改

2.1 反射问题

证明问题所在,具体代码如下

public class InnerClassSingleModel 

    ...

    public static void main(String[] args) throws Exception 
        Constructor<InnerClassSingleModel> constructor = InnerClassSingleModel.class.getDeclaredConstructor();
        InnerClassSingleModel instance1 = constructor.newInstance();
        InnerClassSingleModel instance2 = InnerClassSingleModel.getInstance();
        System.out.println(instance1 == instance2);
    

运行结果如下

false 说明的确可以通过反射来绕过单例。

修复代码如下

import java.lang.reflect.Constructor;

/**
 * 静态内部类
 * @author sai
 * @version 1.0
 * @date 2022/3/27 15:23
 */
public class InnerClassSingleModel 

    private InnerClassSingleModel() 
        if (null != InnerClassSingleModelHandle.INSTANCE) 
            // 因为反射创建实例一样会走构建方法,所以可以在构建方法里面加校验逻辑
            throw new RuntimeException("单例模式不允许通过反射创建实例对象");
        
    

    static class InnerClassSingleModelHandle 
        private static final InnerClassSingleModel INSTANCE = new InnerClassSingleModel();
    

    public static InnerClassSingleModel getInstance() 
        return InnerClassSingleModelHandle.INSTANCE;
    

    public static void main(String[] args) throws Exception 
        Constructor<InnerClassSingleModel> constructor = InnerClassSingleModel.class.getDeclaredConstructor();
        InnerClassSingleModel instance1 = constructor.newInstance();
        InnerClassSingleModel instance2 = InnerClassSingleModel.getInstance();
        System.out.println(instance1 == instance2);
    

2.2 序列化问题

先还原问题,步骤如下:

1、先将类序列化到磁盘,代码如下

import java.io.FileOutputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;

/**
 * 静态内部类
 * @author sai
 * @version 1.0
 * @date 2022/3/27 15:23
 */
public class InnerClassSingleModel implements Serializable 

    private InnerClassSingleModel() 
        if (null != InnerClassSingleModelHandle.INSTANCE) 
            // 因为反射创建实例一样会走构建方法,所以可以在构建方法里面加校验逻辑
            throw new RuntimeException("单例模式不允许通过反射创建实例对象");
        
    

    static class InnerClassSingleModelHandle 
        private static final InnerClassSingleModel INSTANCE = new InnerClassSingleModel();
    

    public static InnerClassSingleModel getInstance() 
        return InnerClassSingleModelHandle.INSTANCE;
    

    public static void main(String[] args) throws Exception 
        InnerClassSingleModel instance = InnerClassSingleModel.getInstance();
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("testSerializable"));
        oos.writeObject(instance);
    

生成了一个序列化文件,如下图

2、通过序列化文件读取类的字节流,生成对象还原问题

public class InnerClassSingleModel implements Serializable 
    ...

    public static void main(String[] args) throws Exception 
        InnerClassSingleModel instance = InnerClassSingleModel.getInstance();
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("testSerializable"));
        InnerClassSingleModel instance2 = (InnerClassSingleModel) ois.readObject();
        System.out.println(instance == instance2);
    

运行结果如下

修复代码如下

package com.czl.java.demo.designmodel;

import java.io.FileInputStream;
import java.io.ObjectInputStream;
import java.io.Serializable;

/**
 * 静态内部类
 * @author sai
 * @version 1.0
 * @date 2022/3/27 15:23
 */
public class InnerClassSingleModel implements Serializable 
    /**
     * 这个是为了兼容版本使用。如现在我有两个成员变量,日后我这个类被修改了,也能做兼容
     * 举个例子:如我现在有 param1 和 param2,我序列化好了。回头我删掉一个字段或者增加个字段,有 serialVersionUID 的话反序列化时一样能够兼容,
     *         否则会抛异常
     */
    private static final long serialVersionUID = -3511142029087501481L;

    /**
     * 这两个字段只是为了说明 serialVersionUID 的作用
     */
    private Integer param1 = 1;
    private String param2 = "1";

    public Integer getParam1() 
        return param1;
    

    public void setParam1(Integer param1) 
        this.param1 = param1;
    

    public String getParam2() 
        return param2;
    

    public void setParam2(String param2) 
        this.param2 = param2;
    

    /**
     * 解决反序列化绕开单例问题,需要增加这个方法,修饰符可以自定义
     */
    private Object readResolve() 
        return getInstance();
    

    private InnerClassSingleModel() 
        if (null != InnerClassSingleModelHandle.INSTANCE) 
            // 因为反射创建实例一样会走构建方法,所以可以在构建方法里面加校验逻辑
            throw new RuntimeException("单例模式不允许通过反射创建实例对象");
        
    

    static class InnerClassSingleModelHandle 
        private static final InnerClassSingleModel INSTANCE = new InnerClassSingleModel();
    

    public static InnerClassSingleModel getInstance() 
        return InnerClassSingleModelHandle.INSTANCE;
    

    public static void main(String[] args) throws Exception 
        InnerClassSingleModel instance = InnerClassSingleModel.getInstance();
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("testSerializable"));
        InnerClassSingleModel instance2 = (InnerClassSingleModel) ois.readObject();
        System.out.println(instance == instance2);
    

以上是关于单例设计模式你真的会用吗?的主要内容,如果未能解决你的问题,请参考以下文章

你真的会用单例模式?

Java中线程池,你真的会用吗?

Unittest 框架之测试固件-----(setUp与tearDown)你真的会用吗?

Java中的List你真的会用吗?

Java单例?kotlin单例?你真的会用单例么?反正我面试过的人会的没几个

极智AI | opencv 你真的会用吗?