Loading

软件设计模式白话文系列(二)单例设计模式

1、描述

确保一个类只有一个实例,并提供对该实例的全局访问。如果你创建了一个对象, 同时过一会儿后你决定再创建一个新对象, 此时你会获得之前已创建的对象, 而不是一个新对象。

这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。

2、实现逻辑

  • 私有化构造方法
  • 提供唯一的公共的获取对象方法

3、实战代码

3.1 饿汉式单例模式

/**
 * 饿汉式单例模式 demo
 *
 * @author Eajur.Wen
 * @version 1.0
 * @date 2022-10-27 16:17:50
 */
public class HungrySingleton {

    private static HungrySingleton singleton = new HungrySingleton();

    private HungrySingleton() {
    }

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

这种实现方式 instance 对象在类加载时创建并初始化,天然的线程安全,但是如果该对象足够大的话,而且不是必须使用的会造成内存浪费,且 GC 时无法回收。

正常开发中,改类型是最多使用的,如果实例占⽤资源多,按照 fail-fast的设计原则(有问题及早暴露),那我们也希望通过饿汉式在程序启动时就将这个实例初始化好。如果资源不够,就会在程序启动的时候触发报错(⽐如 Java 中的 PermGen Space OOM),我们可以⽴即去修复。这样也能避免在程序运⾏⼀段时间后,突然因为初始化这个实例占⽤资源过多,导致系统崩溃,影响系统的可⽤性。

适用场景:对象初始化耗时⻓,不频繁使用的对象。

3.2 懒汉式单例模式

/**
 * 懒汉式单例模式
 *
 * @author Eajur.Wen
 * @version 1.0
 * @date 2022-10-28 14:02:22
 */
public class LazySingleton {

    private static LazySingleton instance;

    private LazySingleton() {
    }

    public static LazySingleton getInstance() {
        if (instance == null) {
            instance = new LazySingleton();
        }

        return instance;
    }
}

这种方式显而易见我们在第一次访问 getInstance() 时,才开始创建对象,解决上面饿汉式不使用时也占用内存的问题,但是又出现了个新的问题,在多线程的情况下,会出现线程安全问题。

3.3 懒汉式单例模式加锁

/**
 * 线程安全的懒汉式单例模式
 *
 * @author Eajur.Wen
 * @version 1.0
 * @date 2022-10-28 14:31:14
 */
public class SynLazySingleton {
    private static SynLazySingleton instance;

    private SynLazySingleton() {
    }

    public static synchronized SynLazySingleton getInstance() {
        if (instance == null) {
            instance = new SynLazySingleton();
        }
        return instance;
    }
}

为了解决线程安全问题,最简单的处理,直接在访问方法添加 synchronized 关键字,这样每个线程都必须持有锁才能访问。但是对于 getInstance() 方法来说,只有在创建对象时才会导致线程安全问题,在第一次访问创建对象后的后续访问是不需要加锁的,为了提高方法后续访问性能,我们需要调整加锁的时机。由此也产生了一种新的实现模式:双重检查锁模式

3.4 双重检查锁模式

/**
 * 双重检查锁单例模式
 *
 * @author Eajur.Wen
 * @version 1.0
 * @date 2022-10-28 14:40:42
 */
public class DoubleCheckLockSingleton {
    private static volatile DoubleCheckLockSingleton instance;

    private DoubleCheckLockSingleton() {
    }

    public static DoubleCheckLockSingleton getInstance() {
        if (instance == null) {
            synchronized (DoubleCheckLockSingleton.class) {
                if (instance == null) {
                    instance = new DoubleCheckLockSingleton();
                }
            }
        }
        return instance;
    }
}

同时我们为了防止 JVM 在实例化对象的时候会进行优化和指令重排序操作时导致的空指针问题,我们需要使用 volatile 关键字,来保证可见性和有序性。这样我们就优雅的解决了单例内存泄漏线程安全还有性能的问题了。

3.5 静态内部类模式

利用 JVM 在加载外部类的时不会加载静态内部类, 只有内部类的属性/方法被调用时才会被加载, 并初始化其静态属性的机制。静态属性由于被 static 修饰,保证只被实例化一次,并且严格保证实例化顺序。

/**
 * 静态内部类模式
 *
 * @author Eajur.Wen
 * @version 1.0
 * @date 2022-10-28 15:07:59
 */
public class StaticInnerClassSingleton {

    private StaticInnerClassSingleton() {
    }

    public static StaticInnerClassSingleton getInstance() {
        return SingletonHolder.INSTANCE;
    }


    static class SingletonHolder {
        private static final StaticInnerClassSingleton INSTANCE = new StaticInnerClassSingleton();
    }
}

静态内部类单例模式是一种优秀的单例模式,是开源项目中比较常用的一种单例模式。在没有加任何锁的情况下,保证了多线程下的安全,并且没有任何性能影响和空间的浪费。

3.6 枚举方式(Java 官方推荐)

在日常开发中,我们经常遇到的枚举也属于饿汉式单例模式的实现,在 JVM 类加载时加载,天然的线程安全,且只会被加载一次。

/**
 * 枚举类型的单例模式
 *
 * @author Eajur.Wen
 * @version 1.0
 * @date 2022-11-07 17:06:02
 */
public enum EnumSingleton {
    INSTANCE;
}

这种实现⽅式通过 Java枚举类型本身的特性,是最简单实现单例的⽅式,保证了实例创建的线程安全性和实例的唯⼀性。

4、如何破坏单例

  • 反射
  • 序列化

4.1 反射破坏单例模式

我们知道单例的本质就是私有化构造方法,然后通过单例类提供的公共方法来获取唯一对象。但是私有化后的构造方法能通过反射轻松获取到,然后执行。

/**
 * 反射破坏单例模式
 *
 * @author Eajur.Wen
 * @version 1.0
 * @date 2022-10-28 15:18:49
 */
public class ReflectionDamage {
    public static void main(String[] args) throws Exception {
        //获取类的字节码对象
        Class clazz = DoubleCheckLockSingleton.class;
        //获取类的私有无参构造方法对象
        Constructor constructor = clazz.getDeclaredConstructor();
        //取消访问检查
        constructor.setAccessible(true);
        DoubleCheckLockSingleton s1 = (DoubleCheckLockSingleton) constructor.newInstance();
        DoubleCheckLockSingleton s2 = (DoubleCheckLockSingleton) constructor.newInstance();

        System.out.println(s1 == s2);
    }
}

得到结果 false

4.2 序列化和反序列化

将对象序列化后再反序列化得到的对象在堆中肯定不是相同地址,而且反序列化也能得到多个对象。明显破坏了单例的模式。

但是反序列化时如果该对象类中存在 readResolve 方法,会将此方法的返回值返回为反序列化的对象,可以通过该机制处理反序列化破坏单例的隐患。

posted @ 2022-10-28 16:41  Eajur  阅读(486)  评论(0编辑  收藏  举报