把书读薄 | 《设计模式之美》设计模式与范式(创建型-单例模式)(中)

简介: 之前做组内分享写过一篇 《重学设计模式 | 单例模式(Singleton Pattern)》,部分参考了《设计模式之美》,故直接搬运,且对此进行一些内容补充,对应 设计模式与范式:创建型(41-43),单例模式是日常开发中是用得最多的模式~ 二手知识加工难免有所纰漏,感兴趣有时间的可自行查阅原文,谢谢。

⑤ 静态内部类(懒加载,线程安全,非常推荐)


public class Singleton {
    private Singleton() { }
    public static Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
    private static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }
}


与饿汉式类似,两者都是通过类加载机制来保证初始化instance时只有一个线程,从而避免线程安全问题。


不同之处是Singleton类被加载时,不会立即初始化,只有调用getInstance()函数时,才会装载SingletonHolder类,从而实例化instance,间接实现了懒加载。


0x3、单例的其他安全问题


上述的单例写法都是围绕着 线程安全问题 进行的,即限制了new创建对象,而Java中除了这种创建对象的方式外,还有三种 克隆、反射和序列化,下面演示下如何通过这三种方式破坏单例。


① 克隆破坏单例


clone()是Object自带函数,每个对象都有,直接调用下clone函数,就能创建一个新对象了,那不就把单例破坏了吗?


答:想太多,被克隆类要实现 Cloneable 接口,然后重写clone()函数,才能完成对象克隆,而一般我们的单例是不会实现这个接口的,所以不存在此问题。


② 反射破坏单例


以静态内部类实现的单例为例,我们通过下述代码构建了两个对象,以此破坏单例:


public class ReflectTest {
    public static void main(String[] args) {
        try {
            Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
            constructor.setAccessible(true);    // 禁用访问安全检查
            Singleton s1 = constructor.newInstance();
            Singleton s2 = constructor.newInstance();
            System.out.println(s1.equals(s2)); // 输出结果:false
        } catch (NoSuchMethodException | IllegalAccessException |
                InstantiationException | InvocationTargetException e) {
            e.printStackTrace();
        }
    }
}


一个最简单的解决方式就是添加一个标志位,当二次调用构造函数时抛出异常,示例如下:


public class Singleton {
    private static boolean flag = true;
    private Singleton() {
        if (flag) {
            flag = !flag;
        } else {
            throw new RuntimeException("有不法之徒想创建第二个实例");
        }
    }
    public static Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
    private static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }
}


此时再运行反射代码:


网络异常,图片无法展示
|


Tips:当然先通过反射修改你的flag,在反射调构造方法依旧是可以破坏的~


③ 序列化破坏单例


同样以静态内部类实现的单例为例,先序列化到文件,然后在反序列化恢复为Java对象:


public class SingletonTest {
    public static void main(String[] args) {
        Singleton singleton1 = Singleton.getInstance();
        Singleton singleton2 = null;
        // 序列化
        try {
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton_file"));
            oos.writeObject(singleton1);
        } catch (IOException e) {
            e.printStackTrace();
        }
        // 反序列化
        try {
            ObjectInputStream ois = new ObjectInputStream(new FileInputStream("singleton_file"));
            try {
                singleton2 = (Singleton) ois.readObject();
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        System.out.println(singleton1 == singleton2);   // 输出:false
    }
}


输出false,单例再次被破坏,接着我们来看下这个新对象是怎么创建出来的,从 readObject 跟到 readOrdinaryObject,定位到下述代码:


网络异常,图片无法展示
|


  • isInstantiable():一个serializable/externalizable的类是否可以在运行时被实例化;
  • desc.newInstance():通过反射的方式调用无参构造函数创建一个新对象;


这就是反序列化破坏单例的原理,接着说下怎么规避,在创建新对象的代码处往下走一些:


网络异常,图片无法展示
|


  • desc.hasReadResolveMethod():判断类是否实现了readResolve()函数;
  • desc.invokeReadResolve(obj):有的反射调用此函数,如果在此函数中返回实例就可以了;


修改后的单例类代码:


import java.io.Serializable;
public class Singleton implements Serializable {
    private Singleton() { }
    public static Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
    private static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }
    private Object readResolve() {
        return getInstance();
    }
}


此时再运行反序列单例时的代码,会输出:true,即同一个对象。


0x4、枚举单例(安全简单,没有懒加载,最佳实践)


上面讲解了除线程安全问题外,三种破坏单例的方式及解决方式,其实用枚举实现单例就能规避这些问题。一个简单的枚举单例代码示例如下:


public enum SingletonEnum {
    INSTANCE;
    private final AtomicLong id = new AtomicLong(0);
    public long getId() {
        return id.incrementAndGet();
    }
}
// 调用
SingletonEnum.INSTANCE.getId()


得益于jdk的enum语法糖,这么简单的代码就能预防这四种问题,接下来一一看下原理。


① 如何保证线程安全


直接在idea上打开生成的SingletonEnum.class文件:


网络异常,图片无法展示
|


好吧,没看到有用的信息,再用JDK自带反编译工具javap编译下:


网络异常,图片无法展示
|


可以看到继承自 Enum类,但是代码不够全,再用jad工具反编译下:


网络异常,图片无法展示
|


反编译后的代码如下:


import java.util.concurrent.atomic.AtomicLong;
public final class SingletonEnum extends Enum {
    public static SingletonEnum[] values() {
        return (SingletonEnum[]) $VALUES.clone();
    }
    public static SingletonEnum valueOf(String name) {
        return (SingletonEnum) Enum.valueOf(SingletonEnum, name);
    }
    private SingletonEnum(String s, int i) {
        super(s, i);
    }
    public long getId() {
        return id.incrementAndGet();
    }
    public static final SingletonEnum INSTANCE;
    private final AtomicLong id = new AtomicLong(0L);
    private static final SingletonEnum $VALUES[];
    static {
        INSTANCE = new SingletonEnum("INSTANCE", 0);
        $VALUES = (new SingletonEnum[]{
                INSTANCE
        });
    }
}


可以看到**INSTANCE的初始化发生在static静态代码块**中,即在类加载阶段执行,保证了线程安全,但跟饿汉式一样,没有懒加载。


相关文章
|
2月前
|
设计模式 安全 Java
Kotlin教程笔记(57) - 改良设计模式 - 单例模式
Kotlin教程笔记(57) - 改良设计模式 - 单例模式
29 2
|
13天前
|
设计模式 Java 数据库连接
Java编程中的设计模式:单例模式的深度剖析
【10月更文挑战第41天】本文深入探讨了Java中广泛使用的单例设计模式,旨在通过简明扼要的语言和实际示例,帮助读者理解其核心原理和应用。文章将介绍单例模式的重要性、实现方式以及在实际应用中如何优雅地处理多线程问题。
27 4
|
22天前
|
设计模式 安全 Java
Kotlin教程笔记(57) - 改良设计模式 - 单例模式
Kotlin教程笔记(57) - 改良设计模式 - 单例模式
|
5天前
|
设计模式 安全 Java
Kotlin教程笔记(57) - 改良设计模式 - 单例模式
Kotlin教程笔记(57) - 改良设计模式 - 单例模式
|
1月前
|
设计模式 存储 数据库连接
PHP中的设计模式:单例模式的深入理解与应用
【10月更文挑战第22天】 在软件开发中,设计模式是解决特定问题的通用解决方案。本文将通过通俗易懂的语言和实例,深入探讨PHP中单例模式的概念、实现方法及其在实际开发中的应用,帮助读者更好地理解和运用这一重要的设计模式。
19 1
|
13天前
|
设计模式 安全 Java
Kotlin教程笔记(57) - 改良设计模式 - 单例模式
Kotlin教程笔记(57) - 改良设计模式 - 单例模式
22 0
|
2月前
|
设计模式 安全 Java
Kotlin教程笔记(57) - 改良设计模式 - 单例模式
Kotlin教程笔记(57) - 改良设计模式 - 单例模式
25 0
|
2月前
|
设计模式 安全 Java
Kotlin教程笔记(57) - 改良设计模式 - 单例模式
本教程详细讲解了Kotlin中的单例模式实现,包括饿汉式、懒汉式、双重检查锁、静态内部类及枚举类等方法,适合需要深入了解Kotlin单例模式的开发者。快速学习者可参考“简洁”系列教程。
33 0
|
2月前
|
设计模式 存储 数据库连接
Python编程中的设计模式之美:单例模式的妙用与实现###
本文将深入浅出地探讨Python编程中的一种重要设计模式——单例模式。通过生动的比喻、清晰的逻辑和实用的代码示例,让读者轻松理解单例模式的核心概念、应用场景及如何在Python中高效实现。无论是初学者还是有经验的开发者,都能从中获得启发,提升对设计模式的理解和应用能力。 ###
|
2月前
|
设计模式 安全 Java
Kotlin教程笔记(57) - 改良设计模式 - 单例模式
Kotlin教程笔记(57) - 改良设计模式 - 单例模式