单例模式的最佳实践居然是使用枚举 ?

单例模式

在我们的系统中,有一些对象其实我们只需要一个,比如说:线程池、缓存、对话框、注册表、日志对象、充当打印机、显卡等设备驱动程序的对象。事实上,这一类对象只能有一个实例,如果制造出多个实例就可能会导致一些问题的产生,比如:程序的行为异常、资源使用过量、或者不一致性的结果。

优势
  • 对于频繁使用的对象,可以省略创建对象所花费的时间,这对于那些重量级对象而言,是非常可观的一笔系统开销;
  • 由于 new 操作的次数减少,因而对系统内存的使用频率也会降低,这将减轻 GC 压力,缩短 GC 停顿时间。

枚举实现单例 — 饿汉式

开门见山, 直接上代码

public enum Singleton {

    // 枚举类的一个实例
    INSTANCE;

    private String name;

    public String getName() {
        return name;
    }
}

获取对象

Singleton s = Singleton.INSTANCE;
分析:

这种方法在功能上与公有域方法相似,但更加简洁,无偿地提供了序列化机制,绝对防止多次实例化,即使是在面对复杂的序列化或者反射攻击的时候。虽然这种方法还没有广泛采用,但是单元素的枚举类型经常成为实现 Singleton的最佳方法。注意,如果Singleton必须扩展一个超类,而不是扩展Enum的时候,则不宜使用这个方法(虽然可以声明枚举去实现接口)。


可能有些人还是迷茫, 解释下枚举

我们再项目中当需要把某些东西一一列举出来时通常会使用枚举, 如订单的各种状态, 异常的各种类型, 方便全局使用.
那么在Java中它是什么呢? 我们通过使用javap -c 看看上面的枚举类
在这里插入图片描述
在这里插入图片描述

我们可以看出在变成字节码之后,变成了类,或者说它本身就是类。继承自Enum类, 我们写的INSTANCE前面加上了static和final, 并且已new实例化, 事实上JVM帮我们写了这么多。 你写的INSTANCE就是枚举类型的一个实例。
此外,编译器会自动帮我们加一个静态方法values(). 发布我们遍历

使用静态代码块或静态变量实现单例模式 — 饿汉式

核心步骤
  1. 私有构造器
  2. 成员变量创建对象
  3. 提供对外获取对象方法

静态变量实现单例模式

public class Singleton {
    private static Singleton singleton = new Singleton();
    
    private Singleton(){
        System.out.println("饿汉式单例: 对象创建成功了.......");
    }
    
    public static Singleton getInstance() {
        return singleton;
    }
}

静态代码块实现单例模式
对象的创建是在静态代码块中,也是对着类的加载而创建。所以和上面方法基本上一样,当然该方式也存在内存浪费问题。

public class Singleton {
    private static Singleton singleton;

    static {
        singleton = new Singleton();
    }

    private Singleton(){}

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

静态内部类方式 — 懒汉式 ( 最佳方法 )

public class Singleton {

    private Singleton(){
        System.out.println("懒汉式单例模式  静态内部类: 创建成功");
    }

    /**
     * 静态内部类单例模式中实例由内部类创建,由于 JVM 在加载外部类的过程中, 是不会加载静态内部类的, 
     * @date: 2023/1/9 16:58
     */
    private static class SingletonHolder{
        private static final Singleton SINGLETON = new Singleton();
    }

    public static Singleton getInstance(){
        return SingletonHolder.SINGLETON;
    }
   
}
说明
  • 静态内部类单例模式中实例由内部类创建,由于 JVM 在加载外部类的过程中, 是不会加载静态内部类的, 只有内部类的属性/方法被调用时才会被加载, 并初始化其静态属性。静态属性由于被 static 修饰,保证只被实例化一次,并且严格保证实例化顺序。
  • 第一次加载Singleton类时不会去初始化INSTANCE,只有第一次调用getInstance,虚拟机加载SingletonHolder, 并初始化INSTANCE,这样不仅能确保线程安全,也能保证 Singleton 类的唯一性。
  • JVM对于类的加载会加类锁,所以多线程情况下也只会保证实例化阶段是一个线程在进行。所以指令重排序就无关紧要了。同时static修饰的资源保证了全局唯一

双重检查锁方式 — 懒汉式 ( 复杂 )

public class Singleton {

    private Singleton() {
        System.out.println("懒汉式 单例模式: 创建成功对象...");
    }

    // 防止 由于jvm指令重排导致空指针异常   (百万并发底概率出现)
    // 通过使用 volatile 保证了指令的可见性和有序性
    private static volatile Singleton singleton;

    /**
     * 提供对外获取对象方法
     * 通过 synchronized 添加同步锁
     * @date: 2023/1/9 11:31
     */
//    public static synchronized Singleton getInstance() {
//        if (singleton == null) {
//            singleton = new Singleton();
//        }
//        return singleton;
//    }

    /**
     * 使用双重锁 提升效率
     * @date: 2023/1/9 12:11
     */
    public static Singleton getInstance() {
        // 第一次判断  在多线程情况下 会有多个通过第一次判断
        if(singleton == null) {
            synchronized (Singleton.class) {
                // 第二次 只允许第一次通过里面的其中一个 去执行创建
                if(singleton == null) {
                    singleton = new Singleton();
                }
            }
        }

        return singleton;
    }
}

说明

由于该方法需要较全面考虑线程安全问题和效率问题, 需要多个关键地方加锁去决绝


单例居然可以强行破解 ( 第一种枚举方式除外 )

在这里插入图片描述

1. 使用反射强制调用私有构造器

    @Test
    public void test2() throws Exception {

        // 1. 获取字节码对象
        Class clazz = Singleton.class;
        // 2. 获取 Singleton 类的私有构造器
        Constructor constructor = clazz.getDeclaredConstructor();
        // 3. 取消检查访问
        constructor.setAccessible(true);
        // 4. 创建对象
        Singleton o = (Singleton) constructor.newInstance();

        System.out.println(o);
        System.out.println(constructor.newInstance());
        System.out.println(constructor.newInstance());
        // 结果对象地址不一样
    }

简单防止方案 在调用构造器方法时判断一下对象是否已经实例化了

    private Singleton() {
        if (singleton != null) {
            throw new RuntimeException("单例对象禁止多次创建");
        }
    }

2. 序列化、反序列方式破坏单例模式

    public void writeObject2File() throws IOException {
        //1 获取对象实例
        Singleton instance = Singleton.getInstance();
        //2 获取输出流
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("C:\\Users\\XiongDa\\Desktop\\a.txt"));
        //3 写入对象
        oos.writeObject(instance);
        //4 关闭
        oos.close();
    }

    public Singleton readObjectFromFile() throws Exception {
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("C:\\Users\\XiongDa\\Desktop\\a.txt"));
        Singleton singleton = (Singleton) ois.readObject();
        ois.close();
        return singleton;
    }
    
    @Test
    public void test() throws Exception {
        // 写入对象
        writeObject2File();
        // 读取
        System.out.println(readObjectFromFile());
        System.out.println(readObjectFromFile());
		// 结果对象地址不一样
    }

防止方案 在Singleton类中添加readResolve()方法,在反序列化时被反射调用,如果定义了这个方法,就返回这个方法的值,如果没有定义,则返回新new出来的对象

public class Singleton {
    private static Singleton singleton = new Singleton();
    
    private Singleton(){
        System.out.println("饿汉式单例: 对象创建成功了.......");
    }
    
    public static Singleton getInstance() {
        return singleton;
    }
    /**
     * 下面是为了解决序列化反序列化破解单例模式
     */
    public Object readResolve() {
        return SingletonHolder.SINGLETON;
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值