Singleton

  • Introduction
  • Implementation
  • ⚠️Vulnerability
    • Reflection Mechanism
    • Serialization/Deserialization
    • Cloneable
    • Multi-thread Access
    • Multiple Class Loaders
    • Garbage Collection
  • Other Implementation Methods
    • Static Inner Class
    • Enum

Introduction

Singleton ensure that a class has only one instance and provide a global point of access to it:

  • Controlled access
  • Reduced namespace - ie. Encapsulate global configuration
  • Flexibility(Can be subclassed - well!) - avoid tight coupling from classes that provides static users

单例模式会给一个类添加许多的职责,这可能违反面向对象设计中的Single Responsibility Principle,该原则要求 “A class should have only one single incentive change in terms of functionality”,给一个单例类添加过多的功能就会违反该原则,但这只是一个 Principle 不是 Rule,虽然会有与之相关的问题,但依旧可以明智的使用单例设计模式。

当希望贯穿应用中体现有且仅有一个的概念时,使用单例模式。Singleton是GOF(Gang of Four)设计模式中运用非常广泛的一个模式。例如:

  • JDK Runtime类使用了Singleton,该类封装了运行时的环境,因此每个java应用程序都只有一个通过getRuntime()方法可获取的Runtime实例,使应用程序能够与其运行的环境相连接,当修改了Runtime中的某个值对于全局都是可见的。
  • 实际的应用场景中,日志管理类使用单例模式,日志的配置信息应当是全局可获得的以便日志模式能够统一。
  • 此外,像是数据库的连接不希望每次做连接数据库操作时就创建新的实例而使用单例模式,并且同时操作多个数据库连接池也会导致程序混乱。

Implementation

  • 确保有且只有一个实例: 限制构造函数 —— 将构造函数设置为私有,并且让此类管理自己的实例
  • 提供全局访问的方法: 提供一个静态方法去获取这唯一的实例
public class Singleton {
    // Eagerly initialization: 该类被加载时,就会创建此对象
    private static Singleton soleInstance = new Singleton();

    // 私有构造器使其他类无法直接通过new创建该类的实例
    private Singleton(){
        System.out.println("Creating...");
    }

    public static Singleton getInstance() {
        return soleInstance;
    }
}
 
class Test {
    public static void main(String[] args) {
        Singleton s1 = Singleton.getInstance();
        Singleton s2 = Singleton.getInstance();
        print("s1", s1);
        print("s2", s2);
    }

    static void print(String name, Singleton object){
        // Hashcode相同说明是同一个instance
        System.out.println(String.format("Object : %s, HashCode : %d", name, object.hashCode());
    }
}

同一个instance: 测试结果

⚠️Vulnerability

Singleton的实现似乎十分容易,其实不然,因为java中有许多潜在的因素很容易会破坏单例模式有且只有一个实例的原则,主要体现在以下几个方面:

Reflection Mechanism

Reflection API可以让我们使用 Introspection 的方式调用构造器或该类的其他方法,因此不通过 new 或者其他类似的方式也可以创建实例:

class Test {
    public static void main(String[] args) throws Exception {
        Singleton s1 = Singleton.getInstance();
        Singleton s2 = Singleton.getInstance();
        print("s1", s1);
        print("s2", s2);

        // reflection
        System.out.println("\nUsing Reflection");
        // load class
        Class clazz = Class.forName("patterns.Singleton");
        Constructor<Singleton> ctor = clazz.getDeclaredConstructor();
        // this api let you violate encapsulation in java and change the access modifier
        ctor.setAccessible(true);

        Singleton s3 = ctor.newInstance();
        print("s3", s3);
    }

    static void print(String name, Singleton object) {
        System.out.println(String.format("Object : %s, HashCode : %d", name, object.hashCode()));
    }
}

单例已被破坏: 测试结果

利用反射机制破坏单例模式的原因在于ctor.setAccessible(true);方法,即把私有的构造器设置为accessible,所以优化的点在构造器,在constructor中检查soleInstance是否已被创建,如果已经创建,则不允许Reflection继续执行并抛出异常。

public class Singleton {
    private static Singleton soleInstance = null;

    // 修改后的构造方法
    private Singleton() {
        if (soleInstance != null) {
            throw new RuntimeException("Cannot create! Please use method getInstance");
        }
        // proceed with creation
        System.out.println("Creating...");
    }

    public static Singleton getInstance() {
        if (soleInstance == null) {
            soleInstance = new Singleton();
        }
        return soleInstance;
    }
}

结果如预期: 测试结果

但是如果在test类中修改执行顺序,单例模式依旧被破坏

public static void main(String[] args) throws Exception {
    // reflection
    System.out.println("\nUsing Reflection");
    // load class
    Class clazz = Class.forName("patterns.Singleton");
    Constructor<Singleton> ctor = clazz.getDeclaredConstructor();
    // this api let you violate encapsulation in java and change the access modifier
    ctor.setAccessible(true);

    Singleton s3 = ctor.newInstance();
    print("s3", s3);

    Singleton s1 = Singleton.getInstance();
    Singleton s2 = Singleton.getInstance();
    print("s1", s1);
    print("s2", s2);

}

测试结果

Serialization/Deserialization

序列化是一个将对象的状态信息写入流的过程,反序列化则相反。

// 在Singleton类实现Serailizable接口,代码略
public class Singleton implements Serializable {}

class Test {
    public static void main(String[] args) throws Exception {
        Singleton s1 = Singleton.getInstance();
        Singleton s2 = Singleton.getInstance();
        print("s1", s1);
        print("s2", s2);

        // Serialization
        System.out.println("\nUsing Serialization");
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("/tmp/s2.ser"));
        oos.writeObject(s2);

        //deserialization
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("/tmp/s2.ser"));
        Singleton s3 = (Singleton) ois.readObject();
        print("s3", s3);

    }

    static void print(String name, Singleton object) {
        System.out.println(String.format("Object : %s, HashCode : %d", name, object.hashCode()));
    }
}

单例已被破坏:
测试结果

此类问题可以通过在 Singleton 中实现 readResolver 方法解决。当反序列化操作完成时,Java 会在此基础上立即调用你重写的readResolve方法,通过这个机制可以改变反序列化的执行结果。

private Object readResolve() throws ObjectStreamException {
        System.out.println(".. read resolve.");
        // change the behavior of deserialization. this will overide deserialization
        return soleInstance;
}

测试结果

Cloneable

// 在Singleton类实现Cloneable接口,并实现默认的clone方法,代码略
public class Singleton implements Cloneable {}

class Test {
    public static void main(String[] args) throws Exception {
        Singleton s1 = Singleton.getInstance();
        Singleton s2 = Singleton.getInstance();
        print("s1", s1);
        print("s2", s2);

        // clone
        System.out.println("\nUsing clone");
        Singleton s3 = (Singleton) s2.clone();
        print("s3", s3);

    }

    static void print(String name, Singleton object) {
        System.out.println(String.format("Object : %s, HashCode : %d", name, object.hashCode()));
    }
}

单例已被破坏:
测试结果

在给单例添加clone机制时就添加了创建多个实例的特性,所以避免它最简单方法就是不支持它,即在clone方法中抛出CloneNotSupportedException。类似序列化与反序列化中可以通过返回soleInstance的方式解决,但是更加推荐抛出异常的方法,因为这两个设计思想本身就是相悖的。

@Override
protected Object clone()  {
    return new CloneNotSupportedException();
}

测试结果

Multi-thread Access

当多个线程同时去创建实例时很容易造成混乱,尤其是出于性能考虑以延迟加载的方式创建时。延迟加载主要有两个优点:

  • 推迟对象创建时间,等直到需要时再创建,这样可以节省资源。
  • 在类加载时创建对象在创建失败时没有机会重新创建,除非重新加载该类,但是使用延迟加载的方式则可以。
public class Singleton {

    private static Singleton soleInstance = null;

    private Singleton() {
        System.out.println("Creating...");
    }

    public static Singleton getInstance() {
        // lazy initialization
        // check-then-act操作
        if (soleInstance == null) { // 操作1
            soleInstance = new Singleton(); // 操作2
        }
        return soleInstance;
    }
}

class Test {
    static void useSingleton() {
        Singleton singleton = Singleton.getInstance();
        print("singleton", singleton);
    }

    public static void main(String[] args) throws Exception {
        System.out.println("\nmulti-threaded access");
        ExecutorService service = Executors.newFixedThreadPool(2);
        // use java 8 lambda expression
        service.submit(Test::useSingleton);
        service.submit(Test::useSingleton);
        service.shutdown();
    }

    static void print(String name, Singleton object) {
        System.out.println(String.format("Object : %s, HashCode : %d", name, object.hashCode()));
    }
}

从单线程应用程序的角度看延迟加载没有问题,但是在多线程环境下,getInstance 方法中的 if 语句形成一个 Check-Then-Act 操作,这不是一个原子操作并且代码中未使用任何同步机制,因此当多线程交错运行时可能出现竞态。

当 soleInstance 值还是 null 的时候,T1 线程和 T2 线程同时执行到操作1,接着 T1 执行操作2前 T2 已率先执行完操作2,因此当 T1 执行到操作2时,尽管 soleInstance 实际已经不为 null,但是 T1 此时仍然会再创建一个实例,因为T1执行操作1时 soleInstance 为 null,这就导致了多个实例的创建,从而破坏单例模式。

测试结果如预期有Work和Not Work两种情况:
测试结果
测试结果

这类问题不难想到通过加同步锁可快速解决:当多个线程同时调用 getInstance 方法时会意识到该方法不可并发执行,所以当第一个线程获得该锁时,其他的线程只能等待并直到第一个线程执行完成,其他线程才可陆续获得该锁,但此时 soleInstance 已被初始化,所以给调用者返回唯一实例。

public static Singleton getInstance() {
    // 使用同步块相比同步方法可以缩小同步的范围
    synchronized(Singleton.class){
        // lazy initialization
        if (soleInstance == null) {
            soleInstance = new Singleton();
        }
    }
    return soleInstance;        
} 

以上固然是线程安全的,但意味着 getInstance 方法的任何一个执行线程都需要申请锁。为了避免锁的开销,可以尝试使用 Double-Checked Locking 进行改进:

 public static Singleton getInstance() {
    // lazy initialization
    // double checked locking
    if (soleInstance == null) { // check1
        synchronized (Singleton.class) {
            if (soleInstance == null) // check2
                soleInstance = new Singleton(); // 操作3
        }
    }
    return soleInstance;
}

尽管 check1 对变量 soleInstance 的访问没有加锁使竞态仍然存在,但它既可以避免锁的开销又能保障线程安全:

T1 线程执行到 check1 时发现soleInstance为null,接着 T1 成功获得锁后继而执行临界区代码,check2 会再次判断soleInstance是否为null,此时由于该线程是在临界区内读取共享变量 soleInstance 的,因此 T1 可以发现此刻soleInstance值不为 null,因此 T1 不会再创建新的实例,从而避免破坏单例模式。

以上从可见性的角度分析结论确实如此,但是考虑到重排序的因素还是可能会出现新的问题无法保障线程安全。

//操作3创建对象的语句可以分解为以下伪代码所示的几个独立子操作:
//子操作1:分配A实例所需的内存空间,并获得一个指向该空间的引用
objRef = allocate(Singleton.class);
//子操作2:调用A类的构造器初始化objRef引用指向的A实例
invokeConstructor(objRef);
//子操作3:将A实例objRef赋值给实例变量a
soleInstance = objRef;

根据临界区内的操作可以在临界区内被重排序的规则,JIT编译器可能将上述的子操作重排序为:子操作1 ——> 子操作3 ——> 子操作2,即在初始化之前将对象的引用写入实例变量soleInstance。

由于锁对有序性的保障是有条件的,而 check1 读取 soleInstance 变量时没有加锁,并且 Java 编译器规范允许应用程序在 Runtime 发布已经初始化的变量供其他线程使用,因此上述重排序对操作1的重排序是有影响的:

  • 该线程可能看到一个未初始化或未初始化完毕的实例,即变量 soleInstance 的值不为 null,但是该变量所引用的对象中的某些实例变量的变量值仍然可能是默认值,而非构造器中设置的初始值。
  • 此时当一个线程在执行操作1的时候发现 soleInstance 不为 null,于是该线程就直接返回这个 soleInstance 变量所引用的实例,而这个实例可能是未初始化完毕的,这就可能导致程序出错。

事实上,Double-Checked Locking目前已经被视为 Anti-Pattern,但是不少现有框架如 Spring 还在使用这种方法,因此知道此方法的相关问题具有实际意义。解决上述问题的方法是通过使用 volatile 关键字修饰 soleInstance 变量。这利用了 volatile 关键字的以下两个作用:

  • 保障可见性:一个线程通过执行操作3修改了soleInstance变量值,其他线程可以读取到相应值(执行check2)
  • 保障有序性:由于volatile能够禁止volatile变量写操作与该操作之前的任何读、写操作进行重排序,因此用volatile修饰的soleInstance相当于禁止JIT编译器以及处理器将子操作2(对对象进行子操作的写操作)重排序到子操作3(将对象引用写入共享变量的写操作)之后,这就保障了一个线程读取到的soleInstance变量所引用的实例时该实例已经初始化完毕。

通过 volatile 关键字对以上两点的保障,Double-Checked Locking 才可正确实现。

public class Singleton {

    private static volatile Singleton soleInstance = null;

    private Singleton() {
        System.out.println("Creating...");
    }

    public static Singleton getInstance() {
        // lazy initialization
        if (soleInstance == null) {
            synchronized (Singleton.class) {
                soleInstance = new Singleton();
            }
        }
        return soleInstance;
    }
}

Multiple Class Loaders

在多 Class Loader 情况下,很难控制单例实例的数量,实现全局的单例。例如:

  • 在 Web 容器中有一个应用程序类加载器和另外一个 Enterprise Archive 类加载器,当这两个类加载器各自独立也非父子关系,如果你试图自己加载此类,并且此时你正在启动你的容器,在 Web 容器下使用的单例,Webapp Class Loader 将会加载这个单例,与此同时你也正在引用你的单例,将也会加载这个单例并且得到一个新的实例,这样你将会在同一个 JVM 创建两个独立的单例实例,从而破坏单例定义。
  • 这也是单例最难修复的问题之一,大多数情况下虽然在理论上已经违反但仅仅也只是 ”Leave it alone and don‘t bother“。在 Single JVM 下,多个 Class Loaders 将会在它们各自的 Class Loading Environment 装载单例的不同的实例,这是很难去避免,网上搜索相关的解决办法是可以在 Singleton 的构造器中添加写代码去“Take certain things in the Class Loader”,但这不是一劳永逸error-free的解决办法,可能在某些特定的情况下不 Work,目前未发现完美的解决方法。
  • 至于为何不使用一个包含静态方法与静态变量的类去创建单例,答案是可以,不过单例有它自己的使用场景,《设计模式》也提及“Sole instance being extensible by subclassing and clients should be able to use an extended instance without modifying their code”。

Garbage Collection

Java 1.2发布后此问题已被解决,在之前更老的版本中,当你再次 new 一个对象时,某些时候已经创建的单例模式的静态实例可能会被垃圾回收器当垃圾回收,并且 new instance API 会返回新的实例。

Other Implementation Methods

Static Inner Class

考虑到 Double-Checked Locking 实现上容易出错,可以采用另外一种同样可以使用实现 Lazy Initialization 且比较简单的方法:

// 基于静态内部类(Static Holder)的单例模式实现
public class Singleton {
    private Singleton() {}

    // singleton holder.
    static class Holder {
        final static Singleton INSTANCE = new Singleton();
    }
    
    public static Singleton getInstance() {
        return Holder.INSTANCE;
    }
}

类的静态变量被初次访问会触发 JVM 对该类进行初始化,即该类的静态变量的值会变成其初始值而不是默认值。因此静态方法getInstance()被调用的时候 JVM 会初始化这个方法所访问的内部静态类 Holder,这使得 Holder 的静态变量 INSTANCE 被初始化,从而使 Singleton 类的唯一实例得以创建。由于类的静态变量只会创建一次,因此 Singleton 也只会被创建一次。

Enum

在《Effective Java》书中建议使用 Enum 实现单例,对于正确实现延迟加载的 Singleton,这是最简单、最robust、最stylish,与此同时或许有些许争议的方法。虽然与 Enum 设计初衷大相径庭,但是它可以解决上诉所有的问题,所以使用enum去创建单例并没有什么错,就日常开发而言,或许可能会让一些不明所以的同学一头雾水吧。

public static enum Singleton {
    INSTANCE;
    // 私有构造器
    Singleton(){}

    public String getConfiguration() {
        return "balabala...";
    }

    public void someService(){}
}

class Test {
    static void useSingleton() {
        Singleton singleton = Singleton.INSTANCE;
        print("singleton", singleton);
    }

    public static void main(String[] args) throws Exception {
        ExecutorService service = Executors.newFixedThreadPool(2);
        service.submit(Test::useSingleton);
        service.submit(Test::useSingleton);
        service.shutdown();
    }

    static void print(String name, Singleton object) {
        System.out.println(String.format("Object : %s, HashCode : %d", name, object.hashCode()));
    }
}

枚举类型Singleton相当于一个单例类,其字段 INSTANCE 相当于该类的唯一实例。此实例是在 Singleton.INSTANCE 初次被引用时才被初始化的。仅访问 Singleton 本身如Singleton.class.getName() 并不会导致 Singleton 的唯一实例被初始化。