I spent a lot of time in Singleton Design Pattern, that's why I am singleton.
饿汉式单例设计模式的实现
实现方法:
- 将构造方法私有化。
- 在类中创建一个本类对象,并用一个私有静态变量引用该对象。
- 提供一个返回该对象的公有静态方法。
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()语句并不是原子性的操作。
创建一个对象可以分为三步:
- 分配对象的内存空间
- 初始化对象
- 设置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单例示例 - 使用枚举实现线程安全的单例