文章目录
单例模式的定义和特点
单例模式(Singleton Pattern)是一种常用的软件设计模式,特别是在面向对象编程(OOP)中。它属于创建型模式的一种,其核心目的是确保一个类在整个应用程序的生命周期中,无论何时何地,都只能生成一个唯一的实例,并提供一个全局访问点供客户端代码获取这个实例。
作为对象的创建模式,单例模式确保其某一个类只有一个实例,而且自行实例化并向整体系统提供这个实例,这个类称为单例类。
例如:Windows中只能打开一个任务管理器,这样可以避免打开多个任务管理器窗口而造成内存资源的浪费,或出现各个窗口显示内容的不一致等错误。
在计算机系统中,还有Windows的回收站、操作系统中的文件系统、多线程中的线程池、显卡的驱动程序对象、打印机的后台处理服务、应用程序的日志对象、数据库的连接池、网站的计数器、Web应用的配置对象、应用程序中的对话框、系统中的缓存等常常被设计成单例。
单例模式有以下特点:
1.单例类只能有一个实例
2.单例类必须自己创建自己的唯一实例
3.单例类必须给其他对象所有对象提供这一实例
单例模式的多种实现方式
1.饿汉式:类加载时即初始化单例实例,优点是线程安全且简单,缺点是可能提前创建了不必要的实例(即客户端未使用),造成资源的浪费。
2.懒汉式:首次调用getInstance()时才创建单例实例,优点是按需创建,缺点是如果不加同步控制,多线程环境下可能会创建多个实例。
3.双重检查锁定:在懒汉式基础上添加双重检查锁(volatile关键字和双重if检查),既实现了延迟初始化又保证了线程安全。
4.静态内部类:利用静态内部类持有单例实例,既保证了线程安全,又实现了延迟初始化,同时无锁性能消耗更优。
5.枚举:使用枚举类型实现单例,不仅线程安全,而且防止了反射和序列化的攻击,被认为是最佳实践之一。(在枚举类型加载过程中,JVM会确保所有的枚举实例在类初次加载时就被创建,并且这个过程是线程安全的)
6.静态代码块:静态代码块在类加载时由JVM自动执行,由于类加载过程是线程安全的,因此这种方式可以保证单例对象的创建只发生一次,提供了安全、高效的访问方式。
7.注册工厂式单例:将HashMap替换为ConcurrentHashMap,map.computeifAbsent()方法具有原子性,可以安全地在并发环境下使用,无需额外的同步措施。通过使用ConcurrentHashMap和computerifAbsent()方法,确保了单例的线程安全和唯一性。
饿汉式
饿汉式在加载的时候就已经完成实例化,之后不再改变,所以是天生的线程安全,可以直接用于多线程而不会出现问题。
public class HungerSingleton {
//私有化构造方法,防止其他类实例化该类
private HungerSingleton(){}
//静态内部变量 ,保存该类的唯一实例
private static final HungerSingleton instance = new HungerSingleton();
//静态工厂方法
public static HungerSingleton getInstance(){
return instance;
}
public void getName()
{
System.out.println("hunger singleton");
}
}
主要特点:
1线程安全性:饿汉式单例在类加载时就完成了实例化,这一步骤由JVM执行,所以是线程安全的。因此即使在多线程环境中,也不会出现并发问题。
2性能效率:由于实例在类加载时就已创建,对于不需要延迟加载的场景,饿汉式单例具有较高的性能效率。但是如果单例对象非常庞大或者初始化过程开销较大,而在程序运行过程中可能并不需要使用这个单例,那么提前初始化可能会造成一定的资源浪费。
3可维护性:构造方法已经私有化,防止外部通过new关键字实例化对象。
懒汉式
懒汉式顾名思义,会延迟加载,在第一次使用该单例的时候才会实例化对象,第一次调用时要做初始化,如果要做的工作比较多,性能上会有些延迟,之后就和饿汉式一样了。
public class LazySingleton {
private LazySingleton(){
//私有构造方法
System.out.println("出来一个懒汉");
}
private static LazySingleton instance=null;
//静态工厂方法
public static LazySingleton getInstance() {
if(instance==null) {
instance=new LazySingleton();
}else {
System.out.println("已经有懒汉了");
}
return instance;
}
public void getName(){
System.out.println("胡八一");
}
}
主要特点:
1延迟初始化(懒加载):懒汉式单例在首次需要使用单例对象时(即首次调用getInstance()方法时)才进行实例化。这种延迟初始化方式可以避免不必要的资源消耗,特别是在单例对象创建成本比较高或者系统启动阶段不需要立即使用单例的情况下,能够有效提升系统性能和减少内存开销。
2线程安全性:懒汉式单例未考虑线程安全问题,当多线程环境下并发调用getInstance()方法时,可能会导致多个实例被创建。(为了保证线程安全,通常需要对getInstance()方法进行同步处理,如使用synchronized关键字修饰方法或块,或者采用双重检查锁定(Double-checked-Locking)等更高效的技术)。
3非实时性:由于懒汉式的延迟初始化特性,当多个线程同时首次请求单例对象时,可能存在短暂的竞争状态,导致部分线程等待实例化过程完成。因此,懒汉式单例的实例化并非实时完成,而是根据实际需求动态触发。
双重检查锁定(DCL)
双重检查锁定单例模式是一种在多线程环境下保证单例模式仅创建一个实例。
创建过程:
1首次检查:先检查实例是否已经被创建,减少不必要的同步开销;
2加锁(同步代码块):避免多线程并发实例化;
3二次检查:防止其他线程在此期间已经创建了实例,确保单例唯一性;
public class DoubleCheckLockSingleton {
private DoubleCheckLockSingleton(){}
private static volatile DoubleCheckLockSingleton instance=null;
public static DoubleCheckLockSingleton getInstance(){
//第一次检查 是为了防止实例已经初始化后还会继续调用同步锁,减少同步代码块的的开销
if (instance==null){
//加锁的目的为了防止多线程同时进入造成对象多次实例化
synchronized (DoubleCheckLockSingleton.class){
//第二次检查是为了防止实例已经初始化后,多个线程同时通过第一次检查,进入同步代码块,造成对象多次实例化
if (instance==null){
instance=new DoubleCheckLockSingleton();
}
}
}
return instance;
}
}
主要特点:
1线程安全性:确保多线程环境中的单例实例过程只发生一次,不会产生多个实例。
2懒加载:实例在首次被请求时才创建,提高了效率。
3高效性:通过减少同步代码块的范围,只在必要时加锁,提升了性能。
4使用volatile:保证了实例变量的可见性和有序性,防止指令重排序导致的问题。(注:volatile关键字声明变量的作用:1保证可见性:当一个线程修改了volatile变量的值,其他线程能够立即看到修改后的值。2禁止指令重排序:防止编译器或处理器对volatile变量的读写操作进行优化重排序。)(-双重检查锁定与延迟加载底层逻辑会在之后的文章进行说明-)
静态内部类
静态内部类单例是一种实现单例模式非常优雅且高效的方法,其特点是结合了懒加载(Lazy Initialization)的优点,既保证了线程安全,又避免了不必要的性能开销。
public class StaticSingleton {
//内部类
private static class SingletonHolder {
//静态常量,储存唯一实例对象
private static final StaticSingleton INSTANCE = new StaticSingleton();
}
//私有构造方法
private StaticSingleton(){}
//公共静态方法
public static final StaticSingleton getInstance(){
return SingletonHolder.INSTANCE;
}
//-----进行额外的扩展--
//静态内部类如果是实现serializable接口单例
//应对反序列化攻击,确保返回的单例是唯一的实例
protected Object readResolve(){
return getInstance();
}
}
主要特点:
1线程安全:不需要加锁机制,静态内部类的实例化是由JVM保证线程安全的。
2懒加载:仅在首次调用getInstance()方法时创建实例,提高了效率。
3简洁高效:避免了同步操作(如synchronized)带来的性能开销。
枚举
在java中,枚举类型(enum)是一种非常简洁且高效的实现单例模式的方式。
枚举单例的实现细节:
1类加载机制:枚举实例在类加载由JVM进行初始化
2唯一保证性:枚举类型只能有一个实例,确保单例的唯一性
3不可继承:枚举类型默认是不可继承的的,防止子类破坏单例模式。
public enum SingletonEnum {
INSTANCE; //定义的唯一枚举实例
public void someMethod(){
System.out.println("枚举类型的单例");
}
private String someFiled;
public void setSomeFiled(String someFiled){
this.someFiled=someFiled;
}
public String getSomeFiled(){
return someFiled;
}
}
主要特点:
1线程安全:枚举实例在类加载时就已经初始化,由JVM确保线程安全。
2序列化安全:枚举实例在序列化和反序列化过程中不会创建新的实例。
3简洁:枚举单例的实现非常简洁,不需要额外的同步机制或者复杂的构造方法。
静态代码块
静态代码块单例是一种实现单例模式的方法,它利用java中的静态代码块在类加载时进行初始化,静态代码块确保类的实例仅被创建一次,静态代码块会在类首次被加载到JVM时执行,因此可以用来初始化类的静态成员,静态代码块单例模式中,类的实例化过程发生在类加载期间,这保证了线实例的唯一性和线程的安全性,因为类加载是由JVM保证线程安全的。
public class StaticBlockSingleton {
// 私有构造函数,防止外部直接实例化
private StaticBlockSingleton() {}
// 定义静态变量用于存储单例对象
private static volatile StaticBlockSingleton instance;
// 静态代码块,在类加载时执行,用于初始化单例对象
static {
instance = new StaticBlockSingleton();
}
public static StaticBlockSingleton getInstance() {
return instance;
}
}
主要特点:
1线程安全:静态代码块在类加载时执行,类加载是由JVM保证线程安全的。
2仅执行一次:类加载机制确保了静态代码块只会执行一次,无论有多少线程尝试访问该类。
3饿汉式:静态代码块通常用于实现饿汉式的单例模式,即在类加载时就创建实例。
缺点是和饿汉式单例一样,无论是否需要这个实例,只要类被加载,实例就会创建,可能会造成资源的浪费。
注册工厂式单例
初始化ConcurrentHashMap:定义一个线程安全的哈希表,用于存储对应的实例。静态代码块:在类加载的时候创建对应的实例,并且将实例及其类名注册到map中。受保护构造器:设置为protected,限制了外部直接实例化的能力,确保单例模式实例化的途径。这种设计允许按需扩展更多单例对象并进行统一的管理。
/**
* 注册工厂式单例
* 将类名注册,下次直接从里面获取
*/
public class RegistSingleton {
//如果使用HashMap在并发场景下是非线程安全的,如果getInstance方法在多线程环境下被调用,可能遇到并发问题,如数据竞争、不一致的状态等
private static ConcurrentHashMap<String, RegistSingleton> map= new ConcurrentHashMap<String, RegistSingleton>();
static {
RegistSingleton singleton = new RegistSingleton();
map.put(singleton.getClass().getName(),singleton);
}
//受保护的构造器
protected RegistSingleton(){}
//提供静态工厂
public static RegistSingleton getInstance(String name){
if (name == null){
name = RegistSingleton.class.getName();
System.out.println("name is null, use default name="+name);
}
//使用concurrentHashMap的computeIfAbsent方法具有原子性,可以安全的在并发环境下使用,无需额外的同步措施
return map.computeIfAbsent(name, RegistSingleton::createInstance);
}
private static RegistSingleton createInstance(String name){
try {
Class<?> clazz = Class.forName(name);
//使用更加安全的反射方式创建实例(减少反射的性能开销)
//--getDeclaredConstructor方法用于确保待实例化的类拥有无参构造器,并通过反射机制安全地创建该类的新实例
return (RegistSingleton) clazz.getDeclaredConstructor().newInstance();
} catch (ClassNotFoundException |InstantiationException | IllegalAccessException | InvocationTargetException |
NoSuchMethodException e) {
//优化异常处理,保留原始异常信息
throw new RuntimeException("错误的创建实例"+name,e);
}
}
}
主要特点:
1单例特性:每个注册类只会有一个实例存在于ConcurrentHashMap中,这符合单例模式的要求,提供了一个全局的访问点。
2线程安全:使用currentHashMap存储实例,确保了在并发环境下也能正确处理实例的创建和获取。
3延迟加载与预初始化:静态代码块中的实例化可以视为预初始化,但如果完整的模式中有方法根据需要从map中获取实例,则可能支持延迟加载。
4扩展性:通过注册工厂注册新的单例对象到map中,使得系统能够灵活的增加新的单例服务。
5封装性:通过保护构造器限制了外部直接实例化,增强了封装性,确保单例对象的创建控制。
小结
优点:
1节省内存空间:单例模式只允许一个实例存在,所以在内存中需要保存一份对象的数据,这对于频繁创建和销毁的对象来说,可以显著减少内存消耗。
2减少性能开销:当一个对象的创建需要较多资源时,如读取配置,创建其他依赖对象等,单例模式可以在应用启动的时候就创建好这个对象并保存内存中,从而避免后续创建和销毁所带来的性能的损耗。
3避免资源多重占用:例如,在写文件操作时,如果只有一个实例存在内存中,就可以避免对同一资源文件同时写入操作。
4全局访问:单例模式提供了一个全局访问点,方便从应用程序的任何地方访问这个唯一的实例,简化了资源共享的复杂度。
5实力控制:单例模式阻止了其他对象实例化自己的单例对象副本,确保所有对象都访问的是同一个实例。
缺点:
1违反单一职责原则:单例类可能变得过于庞大,因为它承担了创建和管理自身实例的责任,同时还可能承担更多的业务逻辑,这违背单一职责原则。
2难以测试:由于单例模式的全局状态特性,使得在单元测试中模拟或者替换单例对象变得困难。
3缺乏抽象:单例模式没有接口,这限制了它的扩展性,因为不能通过继承来扩展单例的行为。
4阻碍并行开发:项目中使用了单例模式,当需要改变单例的行为时,可能涉及项目的多个部分,这增加了并行开发的难度。
5隐藏依赖关系:单例模式可能导致代码中的隐式依赖,使得代码的可读性和可维护性降低。
单例模式在某些场景下是非常有用的,特别是在需要控制资源访问和确保全局唯一的实例情况下,但是过度使用单例模式也可能带来问题,特别是当它导致代码难以测试和维护时。因此,在使用单例模式应该谨慎考虑其适用性和潜在的问题。在现代软件架构中,特别是在支持微服务和容器化的环境中,单例模式的使用应当更加谨慎,因为他们往往要求更高的灵活性和可测试性。