单例模式那些坑

来源:转载

作为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也是抽象类的实现方式,不过该类是静态方法的集合,本身并没有状态,所以这样的实现也勉强合格。



分享给朋友:
您可能感兴趣的文章:
随机阅读: