创建型设计模式-1.单例设计模式
创建型设计模式:核心目的就是给我们提供了一系列全新的创建对象的方式方法
一、简介
1.简述
单例设计模式(Singleton Design Pattern),一个类只允许创建一个对象(或实例),那这个类就是一个单例类,这种设计模式就叫作单例设计模式,简称单例模式。
- 当一个类被设计为单例类时,它只有一个全局访问点,通过该访问点可以获取该类的唯一实例。
- 使用单例模式的主要目的是在整个应用程序中共享一个共享资源或控制对唯一实例的访问。
- 通过限制对象的创建,单例模式确保系统中只有一个实例存在,并提供对该实例的全局访问,从而避免了重复创建对象的开销。
2.使用场景
单例模式适用于以下场景:
- 当只需要一个实例来协调某个操作时,如线程池、日志记录器或缓存管理器。
- 当需要控制资源使用并确保全局访问时,如数据库连接池或文件管理器。
- 当一个对象需要被频繁地访问,但创建和销毁实例的开销较大时。
3.优点和缺点
单例模式的优点包括:
- 提供全局访问点,方便对实例进行统一的管理和控制。
- 减少了重复创建对象的开销,提高了系统的性能。
- 确保在整个应用程序中只有一个实例存在,避免了资源的浪费和冲突。
然而,单例模式也有一些潜在的缺点:
- 单例模式可能会引入全局状态,增加了程序的复杂性和耦合度。
- 单例模式的扩展性有限,如果需要修改单例类的实现,可能需要修改大量的代码。
- 单例模式在多线程环境下需要注意线程安全性的问题,需要确保实例的创建和访问是线程安全的。
总之,单例模式是一种强大的设计模式,适用于需要共享资源或控制实例访问的情况。通过合理地使用单例模式,可以提高应用程序的性能、可维护性和可扩展性。
4.使用步骤
单例设计模式是一种创建型设计模式,它确保一个类只有一个实例,并提供一个全局访问点来访问该实例。以下是创建一个单例的一般步骤:
-
私有化构造函数:将类的构造函数私有化,这样其他类就无法直接实例化该类的对象。
-
创建静态私有实例变量:在类的内部创建一个静态私有变量,用于存储类的唯一实例。
-
创建静态公有方法:提供一个公有的静态方法,用于获取类的实例。这个方法会在内部检查是否已经存在实例,如果存在则直接返回实例,如果不存在则创建一个实例并返回。
下面是一个简单的例子来说明单例设计模式的步骤:
public class Singleton {private static Singleton instance; // 静态私有实例变量private Singleton() {// 私有化构造函数}public static Singleton getInstance() {if (instance == null) { // 检查是否已经存在实例instance = new Singleton(); // 创建实例}return instance; // 返回实例}
}
在上述代码中,构造函数被声明为私有的,确保其他类无法直接实例化Singleton
类的对象。instance
变量是一个静态私有变量,用于存储类的唯一实例。getInstance()
方法是一个静态公有方法,用于获取Singleton
类的实例。在该方法中,首先检查instance
是否为null
,如果为null
则创建一个新的实例,否则直接返回已有的实例。
使用单例模式时,其他类可以通过调用Singleton.getInstance()
来获取Singleton
类的实例,并且每次调用都会返回同一个实例。这样可以确保在整个程序中只有一个Singleton
对象存在。
二、案例
1.饿汉式单例
单例设计模式-饿汉式:
饿汉式是一种简单直接的单例模式实现方式。**在类加载的时候就创建了唯一的实例对象,并在整个生命周期中保持不变。**因此,它也被称为“急切”创建实例的方式。
public class HgySingleton {private static final HgySingleton INSTANCE = new HgySingleton();private HgySingleton() {}public static HgySingleton getInstance() {return INSTANCE;}
}
2.懒汉式单例
单例设计模式-懒汉式:
懒汉式是一种延迟实例化的方式,在第一次访问获取实例的时候才会创建对象。这种方式在实例创建之前没有额外的开销,但需要考虑多线程环境下的线程安全问题。
public class LazySingleton {private static LazySingleton instance = null;private LazySingleton() {}public static LazySingleton getInstance() {if (instance == null) {instance = new LazySingleton();}return instance;}
}
3.双重检查锁单例
单例设计模式-双重检查锁:
双重检查锁是对懒汉式的改进,通过在获取实例时进行双重检查,确保只在实例未创建时才进行同步操作。这样在多线程环境下可以保持高性能并且线程安全。
public class DclSingleton {private static volatile DclSingleton INSTANCE;private DclSingleton() {}public static DclSingleton getInstance() {if (INSTANCE == null) {synchronized (DclSingleton.class) {if (INSTANCE == null) {INSTANCE = new DclSingleton();}}}return INSTANCE;}
}
4.内部类单例
单例设计模式-静态内部类:
静态内部类方式是一种延迟加载实例的方式,它利用了Java类加载机制中的静态内部类不会在外部类加载时被加载的特性。当第一次获取实例时,静态内部类才会被加载并创建实例。
public class InnerSingleton {private InnerSingleton() {}public static InnerSingleton getInstance() {return SingletonHolder.INSTANCE;}private static class SingletonHolder {private static final InnerSingleton INSTANCE = new InnerSingleton();}
}
5.枚举类单例
单例设计模式-枚举类:
枚举类方式是Java中最简洁、高效且线程安全的单例模式实现方式。枚举类的实例是在枚举常量被第一次访问时创建的,而且它天生就是线程安全的。
public class EnumSingleton {private EnumSingleton() {// 私有构造函数}public static EnumSingleton getInstance() {return SingletonEnum.INSTANCE.getInstance();}private static enum SingletonEnum {INSTANCE;private EnumSingleton instant;// 在枚举常量中初始化单例实例private SingletonEnum() {instant = new EnumSingleton();}public EnumSingleton getInstance() {return instant;}}
}
枚举可以抵挡常规的反射攻击是因为Java语言规范对枚举类进行了特殊处理。当使用反射尝试访问枚举类的私有构造函数时,会抛出IllegalAccessException
异常,阻止了对枚举类进行实例化的尝试。
这种反射抵御是通过Java语言规范的设计来实现的。在枚举类中,构造函数被默认为私有的,不允许外部代码直接访问。当使用反射时,如果尝试通过Constructor.newInstance()
方法来调用枚举类的私有构造函数,Java会在内部检查是否为枚举类,并抛出异常。
此外,枚举常量在枚举类被加载时就被实例化,并且是在枚举类初始化阶段完成的。这意味着在第一次访问枚举类时,所有的枚举常量都会被创建,而且无法再通过反射来创建新的枚举常量实例。
综上所述,由于枚举类的特殊处理和枚举常量的提前实例化,枚举可以有效地抵挡常规的反射攻击,确保枚举类的单例特性和唯一性。这也是为什么使用枚举实现单例模式是一种安全可靠的方式。然而,需要注意的是,反射仍然可以访问枚举类的其他成员变量和方法,只是无法通过反射创建新的枚举常量实例。
单例设计模式-反射和序列化:
使用常规的单例模式实现可能会受到反射和序列化的影响而产生多个实例。为了防止这种情况,需要在单例类中做特殊处理,确保在使用反射和序列化时依然保持单例特性。
public class ReflectSerializableSingleton implements Serializable {private static volatile ReflectSerializableSingleton INSTANCE;private ReflectSerializableSingleton() {if (INSTANCE != null) {throw new RuntimeException("实例:【" + this.getClass().getName() + "】已经存在,该实例只允许实例化一次");}}public static ReflectSerializableSingleton getInstance() {if (INSTANCE == null) {synchronized (ReflectSerializableSingleton.class) {if (INSTANCE == null) {INSTANCE = new ReflectSerializableSingleton();}}}return INSTANCE;}// readResolve()方法可以用于替换从流中读取的对象,在进行反序列化时,会尝试执行readResolve方法,并将返回值作为反序列化的结果,而不会克隆一个新的实例,保证jvm中仅仅有一个实例存在public Object readResolve(){return singleton;}
}
三、总结
1.应用
单例模式的应用:
- jdk中有一个类的实现是一个标准单例模式->Runtime类,该类封装了运行时的环境。每个 Java 应用程序都有一个 Runtime 类实例,使应用程序能够与其运行的环境相连接。 一般不能实例化一个Runtime对象,应用程序也不能创建自己的 Runtime 类实例,但可以通过 getRuntime 方法获取当前Runtime运行时对象的引用。
- Mybaits中的org.apache.ibatis.io.VFS使用到了单例模式。VFS就是Virtual File System的意思,mybatis通过VFS来查找指定路径下的资源。查看VFS以及它的实现 类,不难发现,VFS的角色就是对更“底层”的查找指定资源的方法的封装,将复杂的 “底层”操作封装到易于使用的高层模块中,方便使用者使用。
2.存在问题
单例存在的问题:
-
无法支持面向对象编程:单例模式的构造方法被私有化,这导致无法将单例类作为其他类的父类,限制了继承和多态的特性。这意味着在面对未来的需求变化时,扩展性受到限制。如果需要创建类似但略有差异的单例,就需要重新创建一个相似且大部分功能相同的单例类,造成代码冗余。
-
极难的横向扩展:单例模式只允许存在一个实例对象,如果未来需要创建更多实例以满足不同需求,就必须修改源代码。这违反了开闭原则,增加新的实例需要对现有代码进行修改,导致扩展困难。例如,在数据库连接池中,可能从一个连接变成需要多个连接。
-
不同作用范围的单例:
- 线程级别的单例:每个线程都拥有自己的单例实例,线程之间互不干扰。
- 容器范围的单例:在容器中管理单例实例,容器可以管理多个单例对象并控制其生命周期。
- 日志中的多例:在日志记录中,可能需要根据不同的上下文创建多个实例来记录不同的日志信息。
请注意,尽管单例模式存在一些缺点,但在某些情况下仍然是有用的设计模式。在使用单例模式时,需要仔细考虑其适用性和潜在的问题,并根据具体情况做出权衡决策。