设计模式 01 单例模式
2022/7/9 23:24:10
本文主要是介绍设计模式 01 单例模式,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!
参考源
https://www.bilibili.com/video/BV1u3411P7Na?spm_id_from=333.999.0.0&vd_source=299f4bc123b19e7d6f66fefd8f124a03
单例模式(Singleton Pattern)属于创建型模式
顾名思义,单例就是只有一个实例对象。在整个程序中,同一个类始终只会有一个对象来进行操作。
比如数据库连接类,实际上只需要创建一个对象或是直接使用静态方法就可以了,没必要去创建多个对象。
这种模式提供了一种创建对象的最佳方式,涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。
这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
注意点:
- 为保证只能由自身创建对象,单例模式必须构造方法私有化。
- 单例类必须自己创建自己的唯一实例。
- 单例类必须给所有其他对象提供这一实例。
经过多年的演进,单例模式有诸多实现方式,下面逐个介绍。
饿汉式
是否 Lazy 初始化:否
是否多线程安全:是
实现难度:易
本质:以空间换时间
优点:没有加锁,执行效率较高。
缺点:类加载时就初始化,浪费内存。
饿汉式是最简单的单例模式的写法,由于在类创建时就加载,保证了线程的安全。
这种方式比较常用,但容易产生垃圾对象,对空间的消耗较大。
public class Singleton { /** * 单例模式的核心,构造方法私有化 */ private Singleton() { } /** * 用于引用全局唯一的单例对象,在一开始就创建好 */ private final static Singleton INSTANCE = new Singleton(); /** * 获取全局唯一的单例对象 * @return 实例对象 */ public static Singleton getInstance() { return INSTANCE; } }
这种方式最大的问题就是浪费内存,因为创建的对象程序不一定用得到,如果创建了没用到,就是一种浪费。
想要避免这种浪费,自然就想到在使用的时候才创建对象,这样就诞生了懒汉式。
懒汉式
普通懒汉式
是否 Lazy 初始化:是
是否多线程安全:否
实现难度:易
本质:以时间换空间
优点:需要时才创建,不会浪费内存。
缺点:多线程情况下是不安全的,且不是类创建时就加载,效率较饿汉式要低一些。
public class Singleton { /** * 单例模式的核心,构造方法私有化 */ private Singleton() { } /** * 在一开始先不进行对象创建 */ private static Singleton INSTANCE; /** * 获取全局唯一的单例对象 * @return 实例对象 */ public static Singleton getInstance() { // 如果实例为空,那么就进行创建,不为空说明已经创建过了,就直接返回,保证单例 if (INSTANCE == null) { INSTANCE = new Singleton(); } return INSTANCE; } }
这种方式最大的问题就是在多线程情况下不安全,比如下面这样调用:
for (int i = 0; i < 10; i++) { new Thread(()->{ LazyMan.getLazyMan(); }).start(); }
多线程环境下,非空判断容易失效,造成创建多个实例,违背了单例的初衷。
为了避免线程安全问题,还得进行一些改进:
// 方法添加 synchronized 关键字加锁 public static synchronized Singleton getInstance(){ if(INSTANCE == null) { INSTANCE = new Singleton(); } return INSTANCE; }
既然多个线程要调用,那么就直接加一把锁,在方法上添加 synchronized
关键字即可,这样同一时间只能有一个线程进入了。
虽然这样简单粗暴,但是在高并发的情况下,效率肯定是比较低的,可以再进行优化:
public static Singleton getInstance(){ // 规避多线程情况 if(INSTANCE == null) { // 只对赋值这一步进行加锁,提升效率 synchronized (Singleton.class) { INSTANCE = new Singleton(); } } return INSTANCE; }
不过这样还不完美,因为这样还是有可能多个线程同时判断为 null
而进入等锁的状态。
所以,还得加一层内层判断:
public static Singleton getInstance(){ if(INSTANCE == null) { synchronized (Singleton.class) { // 内层检测以实现单例 if(INSTANCE == null) { INSTANCE = new Singleton(); } } } return INSTANCE; }
这样外层的检测用于规避多线程情况,内层的检测用于单例的实现,比较完善的均衡了安全和性能。
但是这种写法在极端情况还是可能会有一定的问题,因为 INSTANCE = new Singleton();
不是原子性操作,它会经过三个步骤:
1、分配对象内存空间。
2、执行构造方法初始化对象。
3、设置 instance 指向刚分配的内存地址,此时 instance != null。
由于指令重排,导致 A 线程执行 INSTANCE = new Singleton();
的时候,可能先执行了第 3 步。
此时线程 B 又进来了,发现 INSTANCE 已经不为空了,直接返回,并且后面使用了返回的 INSTANCE。
由于线程 A 还没有执行第 2 步,导致此时 INSTANCE 还不完整,可能会有一些意想不到的错误。
所以需要增加 volatile 关键字来避免指令重排:
private volatile static Singleton INSTANCE;
这样就实现了完整的双重检测锁(DCL,即 double-checked locking)懒汉式单例模式。
双重检测锁懒汉式
public class Singleton { /** * 单例模式的核心,构造方法私有化 */ private Singleton() { } /** * 在一开始先不进行对象创建 */ private volatile static Singleton INSTANCE; /** * 获取全局唯一的单例对象 * @return 实例对象 */ public static Singleton getInstance(){ // 外层检测规避多线程情况 if(INSTANCE == null) { // 只对赋值这一步进行加锁,提升效率 synchronized (Singleton.class) { // 内层检测以实现单例 if(INSTANCE == null) { INSTANCE = new Singleton(); } } } return INSTANCE; } }
双重校验锁模式虽然严谨,但终究需要加锁,效率始终会受到影响。
那么,有没有一种更好的,不用加锁的方式也能实现延迟加载的写法呢?
答案是有的,可以使用静态内部类实现。
静态内部类实现
public class Singleton { private Singleton() { } // 由静态内部类持有单例对象。根据类加载特性,我们仅使用 Singleton 类时,不会对静态内部类进行初始化 private static class Holder { private final static Singleton INSTANCE = new Singleton(); } // 只有真正使用内部类时,才会进行类初始化 public static Singleton getInstance(){ // 直接获取内部类中的对象 return Holder.INSTANCE; } }
这种方式显然是最完美的懒汉式解决方案:没有进行任何的加锁操作,也能保证线程安全,还能实现懒加载。
不过要实现这种写法,跟语言本身也有一定的关联,并不是所有的语言都支持这种写法的。
而且它也不是绝对安全的,因为 Java 还有一个十分霸道的东西:反射。
万恶的反射
前面的写法尽管已经考虑得很完善,还是忽略了反射
反射是很霸道的,无视 private
修饰的构造方法,可以直接在外面 newInstance
,破坏单例。
Singleton singleton = Singleton.getInstance(); // 获取无参构造器 Constructor<Singleton> declaredConstructor = Singleton.class.getDeclaredConstructor(null); // 取消无参构造器私有属性 declaredConstructor.setAccessible(true); // 使用反射获取的无参构造器实例化对象 Singleton singleton2 = declaredConstructor.newInstance(); // 比较实例化的两个对象 System.out.println(singleton == singleton2);
输出结果为 false,同一个类实例化的两个对象不相等,单例被破坏了。
那么,怎么解决这种问题呢?
可以在构造方法中加上对象的非空判断:
private Singleton() { synchronized (Singleton.class) { if (INSTANCE != null) { throw new RuntimeException("不要试图用反射破坏单例"); } } }
但是这种写法还是有问题:
前面是先正常的调用了 getInstance 方法,创建了 Singleton 对象,然后第二次用反射创建对象,私有构造函数里面的判断起作用了,反射破坏单例模式失败。
但如果先用反射创建对象,判断就不生效了:
// 获取无参构造器 Constructor<Singleton> declaredConstructor = Singleton.class.getDeclaredConstructor(null); // 取消无参构造器私有属性 declaredConstructor.setAccessible(true); // 使用反射获取的无参构造器实例化对象 Singleton singleton1 = declaredConstructor.newInstance(); Singleton singleton2 = declaredConstructor.newInstance(); // 比较实例化的两个对象 System.out.println(singleton == singleton2);
输出结果为 false,同一个类实例化的两个对象不相等,单例又被破坏了!
那么如何防止这种反射破坏呢?
可以使用标志位来避免重复创建:
private static boolean flag = false; private Singleton() { synchronized (Singleton.class) { if (flag == false) { // 第一次创建后,标志位设为 true flag = true; } else { // 后续再使用构造方法创建则直接报错 throw new RuntimeException("不要试图用反射破坏单例模式"); } } }
这样看起来很完美了,但是还是不能完全防止反射破坏单例,因为可以利用反射修改 flag 的值。
// 获取无参构造器 Constructor<Singleton> declaredConstructor = Singleton.class.getDeclaredConstructor(null); // 取消无参构造器私有属性 declaredConstructor.setAccessible(true); // 使用反射获取的无参构造器实例化对象 Singleton singleton1 = declaredConstructor.newInstance(); // 获取 flag 属性 Field field = LazyMan.class.getDeclaredField("flag"); // 取消 flag 属性的私有属性 field.setAccessible(true); // 通过反射,修改属性的值 field.set(singleton1, false); // 使用反射获取的无参构造器实例化对象 Singleton singleton2 = declaredConstructor.newInstance(); // 比较实例化的两个对象 System.out.println(singleton == singleton2);
输出结果为 false,同一个类实例化的两个对象不相等,单例又又被破坏了!
正所谓道高一尺魔高一丈,懒汉式单例不管怎么挣扎,在反射面前都无计可施。
那么,就没有办法完美的实现单例这么一个最简单的设计模式了吗?
答案是有的,但不是外界,而是 Java 自己给出的解决方案。
由于反射是 Java 的语言特性,只能 Java 自身来解决这个问题
在 JDK 1.5 中推出的枚举可以完美解决这个问题。
枚举实现
枚举类型是 JDK 1.5 中新增特性的一部分,它是一种特殊的数据类型,之所以特殊是因为它既是一种类(class)却又比类多了些特殊的约束。
正是这些约束的存在造就了枚举类型的简洁性、安全性以及便捷性。
先来看下枚举是怎么实现单例的:
public enum Singleton { INSTANCE; }
你没有看错,就是这么简单,这也正所谓大道至简!
尝试用反射进行破坏:
Singleton instance1 = Singleton.INSTANCE; Constructor<Singleton> declaredConstructor = Singleton.class.getDeclaredConstructor(null); declaredConstructor.setAccessible(true); Singleton instance2 = declaredConstructor.newInstance(); System.out.println(instance1 == instance2);
意料之外的是,运行报错了:
Exception in thread "main" java.lang.NoSuchMethodException: cn.sail.singleton.enums.Singleton.<init>() at java.lang.Class.getConstructor0(Class.java:3082) at java.lang.Class.getDeclaredConstructor(Class.java:2178)
从报错可以看出,是因为没有找到无参构造方法。
在 IDEA 中查看编译后的 Singleton.class 文件:
public enum Singleton { INSTANCE; private Singleton() { } }
从编译后的文件可以看到,是存在无参构造的,这就奇怪了!
再用 java 命令编译试试:
D:\Project\MY\learn\gof-23\target\classes\cn\sail\singleton\enums> javap -p Singleton.class Compiled from "Singleton.java" public final class cn.sail.singleton.enums.Singleton extends java.lang.Enum<cn.sail.singleton.enums.Singleton> { public static final cn.sail.singleton.enums.Singleton INSTANCE; private static final cn.sail.singleton.enums.Singleton[] $VALUES; public static cn.sail.singleton.enums.Singleton[] values(); public static cn.sail.singleton.enums.Singleton valueOf(java.lang.String); private cn.sail.singleton.enums.Singleton(); static {}; }
从输出结果可以看到,枚举本质还是一个类,只是继承了枚举类。
不过这里还是输出了无参构造:
private cn.sail.singleton.enums.Singleton();
再使用更专业的工具 jad 进行反编译:
D:\Project\MY\learn\gof-23\target\classes\cn\sail\singleton\enums>jad -sjava Singleton.class Parsing Singleton.class... Generating Singleton.java
执行该命令后,会在该目录下生成反编译的 .java 文件:
public final class Singleton extends Enum { public static Singleton[] values() { return (Singleton[])$VALUES.clone(); } public static Singleton valueOf(String name) { return (Singleton)Enum.valueOf(cn/sail/singleton/enums/Singleton, name); } private Singleton(String s, int i) { super(s, i); } public static final Singleton INSTANCE; private static final Singleton $VALUES[]; static { INSTANCE = new Singleton("INSTANCE", 0); $VALUES = (new Singleton[] { INSTANCE }); } }
从这里可以看到,确实是没有无参构造,而是存在一个有参构造:
private Singleton(String s, int i) { super(s, i); }
按照有参构造再使用反射创建:
Singleton instance1 = Singleton.INSTANCE; Constructor<Singleton> declaredConstructor = Singleton.class.getDeclaredConstructor(String.class, int.class); declaredConstructor.setAccessible(true); Singleton instance2 = declaredConstructor.newInstance(); System.out.println(instance1 == instance2);
此时运行也报错,但报的错不一样了:
Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
这就是希望得到的结果:不能用反射破坏枚举。
那么这个报错是怎么实现的呢,其实很简单。
来看下反射中 Constructor 的 newInstance() 源码:
@CallerSensitive public T newInstance(Object ... initargs) throws InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException { if (!override) { if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) { Class<?> caller = Reflection.getCallerClass(); checkAccess(caller, clazz, null, modifiers); } } if ((clazz.getModifiers() & Modifier.ENUM) != 0) throw new IllegalArgumentException("Cannot reflectively create enum objects"); ConstructorAccessor ca = constructorAccessor; if (ca == null) { ca = acquireConstructorAccessor(); } @SuppressWarnings("unchecked") T inst = (T) ca.newInstance(initargs); return inst; }
可以看到,如果类为反射类,就会直接报错。
这就从底层就避免了反射对枚举实现单例的破坏,是最直接也是最有效的手段。
所以,枚举实现是最完美的单例模式。
它极致简洁,自动支持序列化机制,绝对防止多次实例化。
它不仅能避免多线程同步问题,而且还自动支持序列化机制,防止反序列化重新创建新的对象,避免多次实例化。
最让人感动的是,它不会被反射破坏!
这种方式也是 《Effective Java》 作者 Josh Bloch 提倡的方式。
但因为必须 JDK 1.5 及之后的版本才支持这种方式,所以还没有被广泛采用,但这确实是实现单例模式的最佳方法。
这篇关于设计模式 01 单例模式的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!
- 2024-11-23Springboot应用的多环境打包入门
- 2024-11-23Springboot应用的生产发布入门教程
- 2024-11-23Python编程入门指南
- 2024-11-23Java创业入门:从零开始的编程之旅
- 2024-11-23Java创业入门:新手必读的Java编程与创业指南
- 2024-11-23Java对接阿里云智能语音服务入门详解
- 2024-11-23Java对接阿里云智能语音服务入门教程
- 2024-11-23JAVA对接阿里云智能语音服务入门教程
- 2024-11-23Java副业入门:初学者的简单教程
- 2024-11-23JAVA副业入门:初学者的实战指南