作为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也是抽象类的实现方式,不过该类是静态方法的集合,本身并没有状态,所以这样的实现也勉强合格。
分享到:
相关推荐
单例模式最初的定义出现于《设计模式》(艾迪生维斯理, 1994):“保证一个类仅有一个实例,并提供一个访问它的全局访问点。” 而我对单例的理解是,在可控的范围内充当全局变量的作用,就相当于C语言中一个全局...
这个压缩包中的例程可能包括了各种设计模式的应用,如工厂模式、单例模式、装饰器模式、观察者模式等。工厂模式用于创建对象,而单例模式确保一个类只有一个实例。装饰器模式可以在运行时动态地给对象添加新的行为,...
设计模式之 Singleton(单态/单件) 阎宏博士讲解:单例(Singleton)模式 保证一个类只有一个实例,并提供一个访问它的全局访问点 设计模式之 Factory(工厂方法和抽象工厂) 使用工厂模式就象使用 new 一样频繁. ...
- **单例模式**:确保一个类只有一个实例,并提供一个全局访问点。 - **工厂方法模式**:定义一个用于创建对象的接口,让子类决定实例化哪一个类。 - **抽象工厂模式**:提供一个创建一系列相关或相互依赖对象的...
7. **设计模式**:Java中的常见设计模式如工厂模式、单例模式、观察者模式等,是解决特定问题的模板。熟悉并能灵活运用这些模式,能写出更优雅、可维护的代码。 8. **并发工具类**:如Semaphore、CyclicBarrier、...
避坑笔记2021CICDCI/CD流程以及原理说明设计模式:策略模式单例模式工厂模式装饰器模式观察者模式适配器模式模板方法模式SpringBoot:SpringBoot(1):公共配置SpringBoot(2):generatorSpringBoot(3):docker部署...
设计模式共23种(大部分文献也有24种的说法,增加了空对象模式),常用一定要掌握的设计模式:单例模式、工厂模式、抽象工厂模式、策略模式、装饰模式、适配器模式、桥接模式、观察者模式 codetips :closed_book: ...
对象(Object)支持单例模式,可以用来定义全局的、只存在一个实例的类。 Spark是大数据处理框架,选择Scala作为主要编程语言是因为Scala的并发性和函数式编程特性与Spark的高性能计算需求相匹配。尽管Spark也支持...
- **设计模式**:学习并熟练掌握常用的设计模式,如单例模式、工厂模式、观察者模式等,这些模式能帮助解决实际开发中遇到的问题。 - **异常处理**:正确地使用try-catch-finally语句块来捕获和处理异常,确保程序的...
13. **设计模式**:如单例模式、工厂模式、观察者模式等,这些都是解决常见软件设计问题的成熟解决方案。 14. **编程规范和最佳实践**:代码整洁、命名规则、注释规范以及如何写出高质量的Java代码。 通过系统学习...
* Base64 编码与解码、JavaScript面向对象、封装继承、JavaScript 设计模式、单例模式 * 编写 JavaScript 框架、框架结构、JavaScript 模块管理、模块依赖管理工具对比 JavaScript 数据结构 * 数据类型、...
此外,理解和使用设计模式,如单例模式、工厂模式、观察者模式等,可以提高代码的可读性和可维护性。 最后,C++编程过程中应注重代码风格和文档编写,良好的编程习惯能够使代码更容易阅读和理解。同时,持续学习和...
- service:通过service定义的对象默认为单例模式,即整个应用生命周期内,只创建一个该对象实例。service主要用于封装业务逻辑、服务端请求等,并且可以被多次注入到不同的控制器、其他服务或指令中使用。service在...
14. **设计模式**:书中可能涵盖了常见的设计模式,如工厂模式、单例模式、观察者模式等,这些模式能提高代码的可维护性和可扩展性。 通过这些示例代码,开发者可以学习到如何在实际项目中运用C#的特性,理解并掌握...
- 单例模式(Singleton)的实现:保证一个类只有一个实例,并提供一个全局访问点。 #### 9. **方法重载与重写** - 方法重载(Overload)与重写(Override)的区别:重载是在同一类中,参数列表不同的多个方法具有相同的...
- **常用设计模式**:如工厂模式、单例模式、观察者模式等,是解决软件设计中常见问题的成熟解决方案,有助于代码的可读性和可维护性。 - **领域驱动设计(DDD)**:强调将业务逻辑与软件设计相结合,通过定义明确...
9. 设计模式:遵循SOLID原则,项目可能应用了工厂模式、观察者模式、单例模式等设计模式,以提高代码的可扩展性和可维护性。 10. 版本控制:项目很可能使用了Git进行版本控制,以协同开发和管理代码变更。 综上所述...
- 控制器默认为单例模式,在多线程环境下可能存在线程安全问题。为避免这种情况,控制器不应持有状态,即不应有实例变量。 6. **SpringMVC 与 Struts2 的区别**: - 入口点不同:SpringMVC 通过 Servlet,Struts2...
5. **设计模式**:工厂、单例、观察者等设计模式在解决复杂问题时能提供良好的代码结构。 6. **编程语言特性**:掌握至少一种编程语言(如Python、Java、C++或JavaScript)的特性和语法,有助于编写简洁高效的代码...