单例实现方式总结

Java 中实现单例的方式多种多样,本文旨在总结讨论各实现方式的特点与使用场景,但对单例模式本身则不会做过多分析与评价。

实现方式列举

其示例代码为:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
public class EagerInitializedSingleton {
    
    private static final EagerInitializedSingleton instance = new EagerInitializedSingleton();
    
    private EagerInitializedSingleton(){
        // 详细初始化操作
    }

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

这是最常见的实现方式,甚至 Intellij 就会提供这个单例的代码模板,只要选择 Singleton 然后输入名称就能自动生成想要的代码。 另外,要注意的是:构造器并不是只能有一个无参形式的,完全可以传入构造参数。而且,如果需要的化也可以使用多个私有域和私有构造器,对应多种静态工厂方法。

这种实现方式的优点有:

  • 简单直观,编写快速
  • 可以有多种工厂方法用不同方法构造单例内容
  • 线程安全:在载入内存时就进行了初始化,而且 INSTANCE 域不会有修改操作

缺点则有:

  • 不能传入参数
  • 初始化任务太重,不适合大型对象
  • 初始化操作出现异常时,难以处理

实际是静态工厂的一种变体,示例代码为:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
public class StaticBlockSingleton {

    private static StaticBlockSingleton instance;
    
    private StaticBlockSingleton(){
        // 详细初始化操作
    }
    
    static{
        try{
            instance = new StaticBlockSingleton();
        }catch(Exception e){
            throw new RuntimeException("Exception occured in creating singleton instance");
        }
    }
    
    public static StaticBlockSingleton getInstance(){
        return instance;
    }
}

这种方式同原始的静态工厂方法没有太大不同,主要的作用是为初始化操作提供了异常处理机制。

这种方法极其少见,示例代码为:

1
2
3
4
5
6
7

public enum EnumSingleton {

    INSTANCE;

    public static void doSomething(){}
}

Java 中的枚举类型实际也是一种特别的类,可以用它来实现单例模式。而且,同样可以定义各种工作方法。

优点

  • 代码极其简单
  • 线程安全
  • 效率高
  • 不惧怕反射分析和破坏

缺点

  • 不能传入参数
  • 不能延迟加载,载入开销较大

“懒汉式”实现即“延迟加载”(lazy loading),而单重检查模式是其最基本的实现方式,是在“饿汉式”实现上改进而来的,示例代码为:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
public class LazyInitializedSingleton {

    private static LazyInitializedSingleton instance;
    
    private LazyInitializedSingleton(){
        // 详细初始化操作
    }
    
    public static LazyInitializedSingleton getInstance(){
        if(instance == null){
            instance = new LazyInitializedSingleton();
        }
        return instance;
    }
}

这种方式保证了在要使用单例的时候才会进行初始化操作,减轻了程序载入内存时的负担,甚至可以自主决定在适当的时候调用它以进一步优化性能。另外,这种实现方式也可以在 getInstance 中可以加入异常处理语句。 但必须注意的是,多线程环境下这样的实现方法并不安全,多个线程同时调用 getInstance 方法时,可能会为每个线程生成不同的实例。 优点

  • 延迟加载,系统载入负担较轻
  • 异常处理方便
  • 可以传入参数
  • 可以有不同构造方式

缺点

  • 线程不安全

为了应对多线程环境,安全的延迟加载方式,实例代码为:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
public class ThreadSafeSingleton {

    private static ThreadSafeSingleton instance;
    
    private ThreadSafeSingleton(){
        // 详细初始化操作
    }
    
    public static synchronized ThreadSafeSingleton getInstance(){
        if(instance == null){
            instance = new ThreadSafeSingleton();
        }
        return instance;
    }
    
}

这个方法解决了“单重检查”方式的线程不安全问题,但是因为直接在静态方法上加锁导致每一次调用方法获取实例都要进行同步操作,实际运行的效率很低。实际上,在生成了实例之后就没必要进行同步操作了。

优点

  • 延迟加载,系统载入负担较轻
  • 异常处理方便
  • 可以传入参数
  • 可以有不同构造方式
  • 线程安全

缺点

  • 运行效率低

对同步方法进一步改进,得到双重检查模式:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
public class ThreadSafeSingleton {

    private static volatile ThreadSafeSingleton instance;
    
    private ThreadSafeSingleton(){
        // 详细初始化操作
    }
    
    public static ThreadSafeSingleton getInstanceUsingDoubleLocking(){
        if(instance == null){
            synchronized (ThreadSafeSingleton.class) {
                if(instance == null){
                    instance = new ThreadSafeSingleton();
                }
            }
        }
        return instance;
    }
    
}

这种方法只对需要同步的初始化操作进行加锁,提升了运行效率。 要注意的是,Java 5 之前的 JVM 版本 volatile 关键字的实现模式以及 JVM 内存模型与之后的不同,无法保证同步操作的正确实现。

优点

  • 延迟加载,系统载入负担较轻
  • 异常处理方便
  • 可以传入参数
  • 可以有不同构造方式
  • 线程安全
  • 效率较高

缺点

  • 代码稍显复杂,不易理解
  • 不适用于 Java 5 之前的版本

实际通过 Java 的静态内部类的机制,保证了延迟加载:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
public class StaticHolderSingleton {

    private StaticHolderSingleton(){
        // 详细初始化操作
    }
    
    private static class SingletonHolder{
        private static final StaticHolderSingleton INSTANCE = new StaticHolderSingleton();
    }
    
    public static StaticHolderSingleton getInstance(){
        return SingletonHolder.INSTANCE;
    }
}

这个方式并没有手动对任何代码进行同步操作,因而运行效率极高。同时,JVM 实现机制确保了私有静态内部类 SingletonHolder 的延迟加载,而且 JVM 会自动处理多线程访问,保证同步。

优点

  • 延迟加载,系统载入负担较轻
  • 异常处理方便
  • 可以有不同构造方式
  • 线程安全
  • 效率较高

缺点

  • 不能传入参数

安全分析

表面上看只要正确编写代码,单例已经确保了运行环境中只有 1 个实例。但是,Java 提供了强力的反射机制,能够侵入式的分析和获取类的内部信息。借助反射方式可以在既有实例之外构造单例类的新的实例。比如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
public class ReflectionSingletonTest {

    public static void main(String[] args) {
        EagerInitializedSingleton instanceOne = EagerInitializedSingleton.getInstance();
        EagerInitializedSingleton instanceTwo = null;
        try {
            Constructor[] constructors = EagerInitializedSingleton.class.getDeclaredConstructors();
            for (Constructor constructor : constructors) {
                constructor.setAccessible(true);
                instanceTwo = (EagerInitializedSingleton) constructor.newInstance();
                break;
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println(instanceOne.hashCode());
        System.out.println(instanceTwo.hashCode());
    }

}

这段代码对“饿汉式”单例进行反射分析,而后成功构造了单例类的两个不同的实例,破坏了单例模式的约定。其他单例构造方法的反射分析极为相似,不再赘述。

但是,要注意的是:枚举方式实现的单例不惧怕反射分析和破坏,JVM 保证了即使使用反射方法重新建立实例,也和原有单例实例完全相同。

有些时候(尤其在分布式系统环境中)需要对单例进行序列化操作,保存它的信息以便之后进行恢复操作。但是,单例的序列化常常导致生成不同的单例实例,破坏约定。比如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class SerializedSingleton implements Serializable{

    private static final long serialVersionUID = -7604766932017737115L;

    private SerializedSingleton(){}

    private static class SingletonHelper{
        private static final SerializedSingleton instance = new SerializedSingleton();
    }

    public static SerializedSingleton getInstance(){
        return SingletonHelper.instance;
    }
}

public class SingletonSerializedTest {

    public static void main(String[] args) throws FileNotFoundException, IOException, ClassNotFoundException {
        SerializedSingleton instanceOne = SerializedSingleton.getInstance();
        ObjectOutput out = new ObjectOutputStream(new FileOutputStream(
                "filename.ser"));
        out.writeObject(instanceOne);
        out.close();

        ObjectInput in = new ObjectInputStream(new FileInputStream(
                "filename.ser"));
        SerializedSingleton instanceTwo = (SerializedSingleton) in.readObject();
        in.close();

        System.out.println("instanceOne hashCode="+instanceOne.hashCode());
        System.out.println("instanceTwo hashCode="+instanceTwo.hashCode());
    }
}

这个例子中的代码对单例进行序列化操作,之后再进行恢复操作,最后却产生了两个不同的实例。 解决的办法就是再单例的类代码中加入如下方法:

1
2
3
protected Object readResolve() {
    return getInstance();
}

也就是说,要在反序列化操作的时候保证系统中只有一个实例。

另外,枚举方法的单例实现由 JVM 无偿提供序列化机制,无需担心这个问题。

总结

那么,到底实际编程的时候要用哪种单例实现方式呢? 实际编程工作中要考虑以下几个问题:

  • 单例对象是否较“重”,初始化操作消耗资源是否较大?
  • 是否需要在多线程环境中使用?
  • 是否对安全要求很高,或者使用的框架中反射方法极多?
  • 是否需要序列化操作?

通常情况下,单例对象常常负责数据库操作、文件读写、网络访问等,都比较“重”,初始化繁琐且消耗资源极大,所以要考虑使用延迟加载方法:

  1. 如果对线程安全有要求,则优先考虑 static holder 方式
  2. 如果需要传入构造参数,考虑使用双重检查方式
  3. 如果对线程安全没有要求,则使用单重检查方式

另外,如果有一些自定义的操作需要使用单例来管理,初始化消耗资源极少,则可以考虑使用正常加载方式,此时优先使用枚举方式。

参考文章:

Java Singleton Design Pattern Best Practices with Examples