有时候我们只需要一个类只有一个对象,如,线程池、缓存、windows的任务管理器、注册表等,因此就有了单例模式,确保了一个类只存在一个实例。单例模式的实现非常简单,但是其中的细节也需要注意。下面我们就来看看他的各种实现。
单例模式的实现方式有很多,根据是否立即创建对象分为“懒汉”和“饿汉”两大类别,即是否在类加载时立即创建对象,如果该对象频繁被使用,可以使用“饿汉式”提高效率;反之则可以使用“懒汉式”来避免内存的浪费。而“懒汉式”的创建在多线程环境下则有许多方式来保证线程安全。
public class Singleton {
public static Singleton instance;
// 私有化构造方法,保证外部无法创建对象
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
这种只能保证在单线下获取到单例对象,并且在需要的时候才会创建对象,故此称为“懒汉式”。但是因为new Singleton()该操作并不是原子操作,当线程1执行到此时,可能还并未创建实例,那么线程2在判断instance==null时就会为真,从而产生多个实例。
public class Singleton {
private static Singleton instance;
// 私有化构造方法,保证外部无法创建对象
private Singleton() {}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
这种方式保证了线程安全,但是效率非常低,因此一般不推荐使用。
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return instance;
}
}
与懒汉式的区别是类加载的时候立即创建了对象实例,保证了对象始终只会有一个,但是如果该对象一直不被使用,就会浪费内存资源。
public class Singleton {
private Singleton() {}
private static class SingletonInstance {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return SingletonInstance.INSTANCE;
}
}
使用静态内部类来实现单例其实也是懒汉式的优化实现,利用类初始化时线程安全这一特点来创建单例对象,同时因为是在静态内部类中,有且仅当getInstance()方法被调用时才会被初始化,所以也避免了内存的浪费。
public class Singleton {
private static volatile Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
该方式是“饿汉式”的变种,保留了“饿汉式”的特点的同时保证了线程的安全。但是,需要注意的是volatile关键字是必须的,在网上很多文章上看到都没带这个关键字,如果不加可能会导致程序的崩溃。因此该方法只能在JDK1.5后使用。 volatile是保证线程之间的可见性。倘若没有该关键字,假设线程1和线程2先后调用getInstance()方法,当线程1进入方法时判断instance=null,因此去执行new Singleton()创建实例,上文提到该操作并非原子操作,会被编译为三条指令:
而jvm会为了执行效率而进行指令重排,重排后的指令顺序为:1->3->2,当指令执行完第3条指令,此时线程2进入方法进行第一次判断时,就会得到一个并不完整的对象实例(因为对象还未初始化,只是分配了内存空间),接着线程1执行完第2条指令,又会返回这个实例的完全态,但并不会立即刷新主内存,所以线程2并不能访问到,程序就会出现错误导致崩溃。而volatile就是为了处理这个问题,他能保证当某个线程改变对象实例后,立即刷新主内存,让其他线程能够同样获取到相同的实例对象,就不会出现不一致的问题了。
public enum Singleton {
INSTANCE;
}
用枚举的方式创建单例非常简单明了,它本身能保证线程的安全,还能防止反序列化(readObject())导致对象不一致的问题,唯一的缺点则是同饿汉式一样会立即创建对象实例(反编译后可以看到),如果不考虑这点枚举应是单例实现的最佳方式,也是《Effective Java》作者推荐的方式。
单例模式是比较常用的模式之一,本文总结了6种实现方式,可以感受到看似简单的代码背后涉及到的细节非常多,因此也是非常考验我们的基本功。在本文中并没有考虑反射入侵的情况,有兴趣的读者们可自行研究。