How To Destroy Singleton

2014/09/04

Singleton: In software engineering, the singleton pattern is a design pattern that restricts the instantiation of a class to one object. ——Wikipedia

单例的实现方式

单例模式主要有以下几种实现方式:

  1. 枚举方式
  2. 饥饿加载
  3. 静态块方式
  4. 延迟加载
  5. 按需加载

枚举方式

在第二版《Effective Java》中Joshua Bloch说,“单元素枚举是实现Singleton的最佳方式”,所有的JVM都支持枚举。这种方式极容易实现,而且不会出现其他方式拥有的“序列化”问题。

public enum Singleton {
    INSTANCE;
    public void execute (
            String arg  // for example
    ) {
        // Perform operation here
    }
}

这种方式能实现单例的原因,是Java保证任何Enum变量只被实例化一次。Java枚举值是全局可访问的,因此算是被Class Loader延迟加载的单例。它的缺点可能是枚举类型不太灵活。

饥饿加载

如果程序始终需要有一个实例,或者创建实例的开销很大,可以看看饥饿加载,它保证始终都会有一个实例。

public class EagerSingleton {
    private static final EagerSingleton instance = new EagerSingleton();

    private EagerSingleton() {
    }

    public static EagerSingleton getInstance() {
        return instance;
    }
}

这个方法的优势有:

  1. 当该类被使用时,实例才被创建。
  2. 对getInstance()方法不是使用synchronized限制,也就是说所有线程看到的都是同一个实例,没必要使用锁(开销大)。
  3. final关键字说明实例不会被重新定义,保证实例的“有且只有一个”。

静态块方式

与上面的饥饿加载非常类似的一个方式是利用静态块来做一些预处理(例如构造函数异常)。

public class Singleton {
    private static final Singleton instance;

    static {
        try {
            instance = new Singleton();
        } catch (Exception e) {
            throw new RuntimeException("Darn, an error occurred!", e);
        }
    }

    public static Singleton getInstance() {
        return instance;
    }

    private Singleton() {
        // ...
    }
}

延迟加载

本方法使用双重检查,由于一些细微BUG的存在,只能用于J2SE 5.0以后的版本。存在的问题是:在多线程out of order write环境下,instance可能在构造函数Singleton执行前就返回。

public class SingletonDemo {
    private static volatile SingletonDemo instance = null;

    private SingletonDemo() {
    }

    public static SingletonDemo getInstance() {
        if (instance == null) {
            synchronized (SingletonDemo.class) {
                if (instance == null) {
                    instance = new SingletonDemo();
                }
            }
        }
        return instance;
    }
}

另外一个更简单整洁的版本如下,在多线程环境下会有开销大限制并发的问题(锁的开销问题):

public class SingletonDemo {
    private static volatile SingletonDemo instance = null;

    private SingletonDemo() {
    }

    public static synchronized SingletonDemo getInstance() {
        if (instance == null) {
            instance = new SingletonDemo();
        }
        return instance;
    }
}

按需加载

这是比以上所有方式都更懒的一种方式,它利用了语言对类初始化的优势,全部JVM适用。

内部类只会在getInstance()被调用之后才被引用,因此这种方式是线程安全的,而且没有用到如synchronized和volatile之类的特殊语言结构。

public class Singleton {
    private Singleton() {
    }

    /**
     * SingletonHolder is loaded on the first execution of Singleton.getInstance()
     * or the first access to SingletonHolder.INSTANCE, not before.
     */
    private static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }

    public static Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
}

单例模式的破坏

尽管程序员正常使用时并不会突破以上各种实现方式的单例设计,但是利用反射和序列化,我们很容易就可以获得单例类的多个实例。以上除了枚举方式之外,都是将构造函数Private化,从而阻止类外调用构造函数。

反射

反射是多种高级语言都有的特性,也是元编程的核心所在。它允许程序员在运行中获取类结构、方法、字段,并修改Accessible属性,从而给各种Trick带来了可能。

下面将列出利用反射破坏单例模式的代码:

Constructor constructor = LazySingleton.class.getDeclaredConstructor();
constructor.setAccessible(true);
LazySingleton ls1 = (LazySingleton) constructor.newInstance();
LazySingleton ls2 = (LazySingleton) constructor.newInstance();
LazySingleton ls3 = LazySingleton.getInstance();
LazySingleton ls4 = LazySingleton.getInstance();
System.out.println(ls1 == ls2 ? "单例" : "多例"); //输出“多例”
System.out.println(ls3 == ls4 ? "单例" : "多例"); //输出“单例”

反序列化

从输入输出流中读取Object,如ObjectInputStream.readObject(),实际返回是目标类的readResolve()。如果我们的单例类对该函数没有重写,默认将返回该类的新实例。这就是上面几种实现方式具有的“序列化”问题。

public class InnerLazySingleton implements Serializable {
    private InnerLazySingleton() {
    }

    private static class Inner {
        static final InnerLazySingleton instance = new InnerLazySingleton();
    }

    public static synchronized InnerLazySingleton getInstance() {
        return Inner.instance;
    }
}

public class Test {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        InnerLazySingleton ils = InnerLazySingleton.getInstance();
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(baos);
        oos.writeObject(ils);

        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray()));
        InnerLazySingleton ils2 = (InnerLazySingleton) ois.readObject();
        System.out.println((ils == ils2 ? "单例" : "多例")); //输出“多例”
    }
}

如果在单例类中重写readResolve(),如下,则能达到单例效果。

public class InnerLazySingleton implements Serializable {
    private InnerLazySingleton() {
    }

    private static class Inner {
        static final InnerLazySingleton instance = new InnerLazySingleton();
    }

    public static synchronized InnerLazySingleton getInstance() {
        return Inner.instance;
    }

    private Object readResolve() {
        return getInstance();
    }
}

扩展

单例和抽象工厂一起使用

单例模式经常和抽象工厂模式一起使用,来创建一个全局可用但使用者不知道其详细类型的资源。合用这两种模式的一个例子就是Java Abstract Window Tookit(AWT)。

java.awt.Toolkit是将各种AWT组件绑定到特定本地实现的一个抽象类,它有一个Toolkit.getDefaultToolkit()工厂方法返回基于平台实现的子类Toolkit。因为AWT只需要一个对象来执行绑定,而且该对象创建代价较高,所以这个Toolkit被实现为单例。

比较常见的是工厂+单例模式,工厂类一般都被实现为单例。