单例模式那些坑
作为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. 使用抽象类实现单例
上一篇: Python量化交易之四_聚宽数据
下一篇: 聚石塔RDS数据备份与迁移