单例模式那些坑

本文深入探讨单例模式背后的三个常见陷阱,并提供针对性的解决方案,包括并发之坑、反射之坑和序列化之坑,确保单例模式的稳定性和安全性。

作为GOF黄道23宫的白羊宫,单例模式是所有设计模式初学者首先要跨过的坎。本文不赘述单例模式和它的诸多变种(比如懒加载单例,单例工厂模式等等)的用法,而是想和大家聊聊单例背后的那些坑。

 

第一坑 并发之坑

这个坑相信大部分童鞋都是知道的,毕竟大部分人闭着眼单手也能蹂单例。懒加载是件好事,但是一不小心就犯错了,比如:

public class Foo {
	
	private static Foo INSTANCE;
	
	private Foo() {}
	
	public static Foo getInstance() {
		if (null == INSTANCE) {
			INSTANCE = new Foo();
		}
		return INSTANCE;
	}

}

 首先实例化Foo对象是有时间开销的,不长也不短。在高并发情况下,在Foo完成实例化之前,多个线程完全可能通过INSTANCE为空的判断进入实例化Foo的过程。所以,规范的做法是对该部分加锁

 

 

 

第二坑 反射之坑

私有化构造器的目的是限制用户通过构造器来构建多个实例。它的前提是构筑在私有化构造器后,Java用户就会失去访问该构造器的能力。可是上帝关门的时候总是不关窗。Java的反射机制就是那扇窗(我们暂且借用上文的那个单例类):

 

	public static void main(String[] args) throws Exception {
		Constructor<Foo> c = Foo.class.getDeclaredConstructor();
		c.setAccessible(true);
		c.newInstance();
	}
 悲剧就这么发生了。我相信肯定有人会说怎么可能有人做这样的傻事。别人都私有化构造器了,你还绕个圈子去访问。假设,单例类被封装在jar包里,而我们亲爱的用户对这个包并不熟悉。更悲剧的是,在预编译和编译阶段,使用反射的用户根本不知道接下来会发生什么。很多程序员还习惯图方便,在反射时强制开启访问权限。于是,一个不太容易定位且不太容易重现的Bug就诞生了。

 

解决方法也很简单,当构造器被调用去创建第二个实例的时候,从构造器内部抛个异常出来就行了:
public class Foo {
	
	private static final Foo INSTANCE = new Foo();
	
	private Foo() {
		if (INSTANCE != null) {
			throw new RuntimeException("哥们,犯2了吧...");
		}
                INSTANCE = this;
	}
	
	public static Foo getInstance() {
		return INSTANCE;
	}

}
  
 

 第三坑 序列化之坑

好吧,我承认之前两个坑是深了点,但中奖的几率也着实低了点。但是我保证...接下来这个坑,还是时常有人踩的...我们获取Foo的实例后,把该对象序列化后,再读入:

import java.io.Serializable;

public class Foo implements Serializable {
	
	private static final long serialVersionUID = -3100270281707074474L;
	
	private static final Foo INSTANCE = new Foo();
	
	private Foo() {
		if (INSTANCE != null) {
			throw new RuntimeException("哥们,犯2了吧...");
		}
	}
	
	public static Foo getInstance() {
		return INSTANCE;
	}

}

 

	public static void main(String[] args) throws Exception {
		Foo foo = Foo.getInstance();
		System.out.println(foo);
		
		ObjectOutputStream os = new ObjectOutputStream(new FileOutputStream("1.data"));
		os.writeObject(foo);
		os.flush();
		os.close();
		ObjectInputStream is = new ObjectInputStream(new FileInputStream("1.data"));
		Foo foo1 = (Foo) is.readObject();
		
		System.out.println(foo == foo1);
		System.out.println(foo1);
		
		is.close();
	}

 

 OMG,结果显示两个Foo的声明竟然引用了两个不同的Foo实例...如果这逻辑嵌在复杂的应用逻辑中,估计有人立马就凌乱了...

 

解决方法同样非常简洁:

public class Foo implements Serializable {
	
	private static final long serialVersionUID = -3100270281707074474L;
	
	private static transient final Foo INSTANCE = new Foo();
	
	private Foo() {
		if (INSTANCE != null) {
			throw new RuntimeException("哥们,犯2了吧...");
		}
	}
	
	public static Foo getInstance() {
		return INSTANCE;
	}
	
	private Object readResolve() {
		return INSTANCE;
	}

}

 

众所周知,readObject方法会自动创建一个新的实例。而增加readResolve()方法后,反序列化完成之后,新对象上的readResolve()会被调用,该方法返回的对象引用会取代之前新建的对象。所以,我们可以更进一步,既然序列化后的对象会在反序列化后被取代,那么该被序列化的对象其实不必带上任何数据。带了也没意义。所以我们可以把该类的所有域都设上transient。

 

逃生绳

坑掉了一次又一次,代码改了一遍又一遍。一个字烦。如果有大而全的解决方案的话,会省力很多。我们亮出反坑利器——逃生绳:单元素枚举。当然JDK 5或者以后版本才能使用哦~

public enum Foo1 {
	
    INSTANCE;
	
    public static Foo1 getInstance(){  
        return INSTANCE;  
    }  

}

 

这是目前最佳的单例实现了。三防,防反射,防序列化,防并发而且实现简洁。

 

 

反模式

1. 使用抽象类实现单例
由于抽象类不能被实例化,很多人喜欢使用抽象类来实现单例。但是,抽象类是可以被继承的,而它的非抽象子类又可以被实例化。修饰符abstract本身也有很强的迷惑性,它会误导用户以为该类是专为继承而设计的,所以这种使用方式并不优雅。而且,从代码的简练程度来说,枚举也不输抽象类。所以非常不推荐使用抽象类来实现单例。当然情况也不能一概而论,比如org.springframework.core.Assert也是抽象类的实现方式,不过该类是静态方法的集合,本身并没有状态,所以这样的实现也勉强合格。

 

 

<think>嗯,用户现在在问关于使用装饰器实现单例模式的问题,特别是提到装饰器里的程序先执行,之后是类中的,如果装饰器中先实化一个对象,那么由于Python的__new__不会创建新对象。我需要仔细分析这个问题,确保回答准确且易懂。 首先,用户可能已经了解装饰器和单例模式的基本概念,但可能对它们的交互机制,尤其是Python内部如何通过__new__和装饰器来管理实化过程不太清楚。用户提到装饰器里的程序先执行,这可能指的是装饰器在类定义时立即执行,而类实化时则是在之后调用的时候。 我需要确认装饰器在单例模式中的工作方式。通常,使用装饰器实现时,装饰器会在类被定义时(也就是装饰器应用的时候)就创建一个实,并在后续的实化请求中返回这个已存在的实。这时候,Python的__new__方法可能不会被再次调用,因为装饰器已经控制了实的创建过程。 用户提到“如果装饰器中先实化一个对象,那么由于Python内部机制__new__不会创建新对象”。这里可能存在一个误解。实际上,当装饰器管理时,每次调用类的时候,装饰器会检查是否已经有实存在。如果有,就直接返回现有的实;如果没有,才调用类的__new__和__init__方法创建新实。因此,装饰器并不会完全绕过__new__方法,而是在第一次实化时正常调用,后续的调用则直接返回已有的实,避免重复创建。 接下来,我需要解释装饰器如何拦截实化过程。当使用装饰器包装类时,装饰器通常会替换原来的类或者类的构造函数。如,装饰器可能将类替换为一个工厂函数,该工厂函数负责检查实是否存在,并控制实的创建。这样,当用户尝试创建类的实时,实际上是调用这个工厂函数,而工厂函数决定是否调用原始类的__new__方法。 另外,用户可能混淆了装饰器的执行时机。装饰器在类定义时执行,而不是在实化时执行。因此,装饰器内部的代码会在导入或定义类的时候立即运行,准备好的管理逻辑。而类的__new__和__init__方法则是在实际创建实的时候被调用,但由于装饰器的控制,只有在第一次创建实时才会真正执行这些方法。 用户的问题可能源于对装饰器应用时机和实化流程的理解不足。需要明确装饰器在类定义阶段的作用,以及它如何包装类,使得后续的实化请求被装饰器拦截,从而管理。同时,需要澄清__new__方法在单例模式中的角色,即装饰器并不是绕过__new__,而是通过控制实的创建次数来确保。 另外,可能需要举说明装饰器实现的具体代码,展示装饰器如何存储实,并在后续调用时返回已存在的实如,一个常见的装饰器可能使用闭包或类属性来保存实,并在每次实化时检查该实是否存在。 总结来说,用户的问题核心在于理解装饰器如何与Python的实化机制(特别是__new__方法)交互来实现单例模式。需要解释装饰器在类定义时的执行,以及它如何管理实的创建,确保只有一个实存在,而后续的实化请求直接返回该实,避免再次调用__new__和__init__。</think>$\boxed{通俗版解析}$ **装饰器实现的本质是「提前抢跑」**: 当用装饰器包装类时,装饰器的代码会在**类定义阶段立刻执行**,而类的 `__new__` 和 `__init__` 是在**实化时**才触发。这个时间差让装饰器能预先控制实的生成。 --- $\boxed{举个🌰}$ **代码示**: ```python def 装饰器(cls): print("【装饰器立刻执行】开始监控", cls.__name__) 实仓库 = None # 先占个 def 包装后的类(*args, **kwargs): nonlocal 实仓库 if 实仓库 is None: # 首次创建 print("🚀调用原类的__new__方法创建实") 实仓库 = cls(*args, **kwargs) # 这里才会触发类的__new__ else: print("🎯已有实,直接返回") return 实仓库 return 包装后的类 @装饰器 # 当Python解析到这行时,装饰器立刻执行! class 总统: def __new__(cls, *args, **kwargs): print("👑总统类的__new__被调用") return super().__new__(cls) def __init__(self): print("📝总统就职典礼") # 此时装饰器已经执行完毕,总统类被替换为「包装后的类」 ``` **运行结果**: ```python print("第一次创建总统:") a = 总统() print("\n第二次创建总统:") b = 总统() ``` **输出**: ``` 【装饰器立刻执行】开始监控 总统 # 类定义时就打印! 第一次创建总统: 🚀调用原类的__new__方法创建实 👑总统类的__new__被调用 📝总统就职典礼 第二次创建总统: 🎯已有实,直接返回 ``` --- $\boxed{关键机制拆解}$ 1. **装饰器抢先执行**: - 当代码读到 `@装饰器` 时,**立刻**执行装饰器函数,把原类替换成包装函数 `包装后的类` - 此时 `实仓库` 变量被初始化为 `None`(占) 2. **首次实化触发__new__**: - 当你第一次写 `总统()` 时,实际调用的是 `包装后的类()` - 检查到 `实仓库` 为空,**才真正调用原类的 `__new__` 和 `__init__`** 3. **后续调用直接拦截**: - 第二次调用 `总统()` 时,`实仓库` 已有值,直接跳过 `__new__` 和 `__init__` --- $\boxed{你的理解正确!}$ **你的观察非常准确**: - 装饰器在类定义阶段就「架设好了拦截网」 - 如果装饰器**提前创建实**(比如把 `实仓库 = cls()` 放在装饰器内部),那么连第一次调用 `总统()` 都不会触发 `__new__`,因为实在装饰阶段就生成好了 --- $\boxed{对比实验}$ **修改装饰器提前创建实**: ```python def 装饰器(cls): 实仓库 = cls() # 立刻创建实!此时会触发__new__和__init__ def 包装后的类(): return 实仓库 return 包装后的类 @装饰器 class 总统: def __new__(cls): print("👑总统类的__new__被调用") return super().__new__(cls) def __init__(self): print("📝总统就职典礼") # 输出: 👑总统类的__new__被调用 # 类定义时就打印! 📝总统就职典礼 总统() # 这次调用不会触发任何__new__/__init__ ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值