Java中单例设计模式的实现

I spent a lot of time in Singleton Design Pattern, that's why I am singleton.

饿汉式单例设计模式的实现

实现方法:

  1. 将构造方法私有化。
  2. 在类中创建一个本类对象,并用一个私有静态变量引用该对象。
  3. 提供一个返回该对象的公有静态方法。
class Singleton {
    private static final Singleton instance = new Singleton();

    private Singleton() {
    }

    public static Singleton getInstance() {
        return instance;
    }

    public static void singletonOperation() {
        //do something
    }
}

单例类图
饿汉式的实现是线程安全的。参见:Java单例示例 - 使用静态变量初始化实现线程安全的单例
存在的问题:如果单例类实例的创建是依赖外部参数或配置文件的,就不能使用饿汉式单例设计模式了,因为类装载时实例就创建了,来不及调用其中的方法来获取外部参数或配置文件。

懒汉式单例设计模式的实现

非线程安全版

class Singleton {
    private static Singleton instance;

    private Singleton() {
    }

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

    public static void singletonOperation() {
        //do something
    }
}

上面的实现在多线程环境下不能正常工作。假设有两个线程初次并行调用Singleton.getInstance()方法,两个线程都同时执行到if (instance == null),这时instance还不是null,因此会创建两个不同的实例,不符合单例设计模式的要求。

用同步锁解决线程安全问题

为了保证线程安全,可以加同步锁来解决问题。

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

但是这样的实现并不高效。当线程试图获取某个对象的同步锁时,如果该锁被其他线程所持有,则当前线程会进入阻塞状态。也就是在任意时刻,只有一个线程能调用Singleton.getInstance()方法,其他试图调用该方法的线程会进入阻塞状态。

双重检查锁方法

不过同步操作只有第一次调用Singleton.getInstance()方法时才需要用到,所以我们可以换用下面的写法,也被称为双重检查锁(Double-Check Locking)。

class Singleton {
    private volatile static Singleton instance;  //声明成 volatile

    private Singleton() {
    }

    public static Singleton getSingleton() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
    
    public static void singletonOperation() {
        //do something
    }
}

假设有两个线程初次并行调用Singleton.getInstance()方法,两个线程都同时执行到第一个if (instance == null),这时instance还不是null,所以都继续往下执行。但是后续的代码加了同步锁,确保同一时刻只有一个线程能执行后面的instance = new Singleton()语句。等其中一个线程执行了instance = new Singleton()语句,这时候instance不为null了,所以另一个线程也无法进入第二个if (instance == null)语句块了,而是执行后续的return instance语句,这样就保证了该类只有唯一的实例。

但是如果private volatile static Singleton instance语句不加volatile关键字,仍然会存在问题,这是为什么呢?
主要原因在于instance = new Singleton()语句并不是原子性的操作。
创建一个对象可以分为三步:

  1. 分配对象的内存空间
  2. 初始化对象
  3. 设置instance指向刚分配的内存地址

当instance指向分配地址时,instance不为null。
但是在 JVM 的即时编译器中存在指令重排的优化。也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是1-2-3也可能是1-3-2。如果是1-3-2,那么在多线程环境下就会出现问题。
下面是流程图和我的分析。
流程图
当第一个线程A执行到new Singleton()子过程的第二步,此时instance不为null了,但是该对象并没有初始化完毕,而另外一个线程B刚好执行到第一个if (instance == null)语句块,这时线程B就会得到一个没有正确初始化的对象。线程B对这个未正确初始化的对象进行操作,可能会导致异常的发生。
注意以上方法必须在Java 5.0以上版本才能保证不出问题。其原因是 Java 5.0 以前的 JMM(Java 内存模型)是存在缺陷的,即使将变量声明成volatile也不能完全避免指令重排,这个问题在Java 5.0中才得以修复。

更优雅的办法,Initialization on Demand Holder(IoDH)

class Singleton {
    private Singleton() {
    }

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

    public static void singletonOperation() {
        //do something
    }

    private static class SingletonHolder {
        private static final Singleton instance = new Singleton();
    }
}

这里使用到了静态内部类,静态内部类在外部类被加载的时候不会被同时加载,当且仅当其某个静态成员被调用时才会被加载,这样就达到了延迟加载的目的。
同饿汉式单例设计模式一样,都是基于类加载机制避免了多线程的同步问题,保证初始化实例时只有一个线程。

使用枚举

还可以使用枚举类来实现单例设计模式。

public enum Singleton {
    INSTANCE;

    public static void singletonOperation() {
        //do something
    }
}

通过调用Singleton.INSTANCE来获得Singleton类的实例。
创建枚举实例默认就是线程安全的,而且还能防止反序列化导致重新创建新的对象。
参见:Java单例示例 - 使用枚举实现线程安全的单例

comments powered by Disqus