单例模式(Singleton)
- 描述
对一些类或资源,如打印机,它仅有单一的实例可用。故在一些类的设计时,保证一个类仅有一个实例,并提供一个全局访问点,这是一种常见的设计模式,即Singleton模式。
在类中保存一个单一的实例对象,保证没有其他实例可以被创建(通过截取创建新对象的请求),并且它可以提供一个访问该实例的方法。
- 实现
Singleton模式的实现方式可分为两大类:饿汉模式和懒汉模式。
-
饿汉模式:在第一次引用该类的时候就创建对象实例,而不管实际是否需要创建。
public class Singleton {
private static final Singleton singleton = new Singleton();
private Singleton() {};
public static Singleton getInstance() {
return singleton;
}
}这种方式的优点是实现简单,缺点是不管我们有没有用到该对象实例,它都会被创建。
-
懒汉模式:
a. 简单懒加载方式:实现简单且使用了懒加载模式,但是当多线程并行调用
getInstance()
时,会创建多个实例,即在多线程下不能正常工作。public class Singleton { private static Singleton singleton = null; private Singleton(){} public static Singleton getSingleton() { if(singleton == null) singleton = new Singleton(); return singleton; } }
b. 同步锁方式:做到了线程安全,并且能保证单一实例,但是它并不高效。因为在任何时候只能有一个线程调用
getInstance()
方法,但是同步操作只有在第一次创建单例对象时才需要。public class Singleton { private static Singleton singleton = null; private Singleton() {} public static synchronized Singleton getSingleton() { if(singleton == null) singleton = new Singleton(); return singleton; } }
c. 双重检查锁方式:在
getInstance()
方法中,进行两次null检查。极大提升了并发度,进而提升了性能。为什么可以提高并发度呢?在单例模式中只有在第一次创建单例时需要同步加锁,而绝大多数操作都是可以并行的。因此在加锁前多进行一次null检查就可以减少绝大多数的加锁操作,进而达到提高执行效率提高的目的。public class Singleton { private static volatile Singleton singleton = null; private Singleton() {}; public static Singleton getInstance() { if(singleton == null) { synchronized(Singleton.class){ if(singleton == null){ singleton = new Singleton(); } } } return singleton; } }
注:
singleton
实例变量必须用volatile
进行修饰。原因主要在于singleton = new Singleton()
并非是一个原子操作,实际在JVM中这句话大概做了下面3件事情。- 给singleton分配内存 - 调用Singleton的构造函数来初始化成员变量 - 将singletone对象指向分配的内存空间(执行完这步singleton就为非null了)
在 JVM 的即时编译器中存在指令重排序的优化,也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是后者,则在 3 执行完毕而 2 未执行之前,被线程二抢占了,这时 singleton 已经是非 null 了(但却没有初始化),所以线程二会直接返回 singleton,然后使用,则会报错。
d. 静态内部类:把Singleton实例放到一个静态内部类中,这样既避免了在Singleton类加载的时候就创建静态实例,并且由于静态内部类只会被加载一次,所以这种写法也是线程安全的。
public class Singleton { private static class Holder { private static Singleton singleton = new Singleton(); } private Singleton() {} public static Singleton getSingleton(){ return Holder.singleton; } }
上面提到的所有实现方式都有两个共同的缺点:1) 都需要额外的工作(Serializable、transient、readResolve())来实现序列化,否则每次反序列化一个序列化的对象实例时都会创建一个新的实例。2) 可能会有人使用反射强行调用我们的私有构造器(如果要避免这种情况,可以修改构造器,让它在创建第二个实例的时候抛异常)。而枚举方式可以解决这两个问题:
e. 枚举方式:使用枚举除了线程安全和防止反射强行调用构造器之外,还提供了自动序列化机制,防止反序列化的时候创建新的对象实例。可以通过Singleton.INSTANCE来访问实例,且创建枚举默认就是线程安全的。
public enum Singleton { INSTANCE; private String name; public String getName(){ return name; } public void setName(String name){ this.name = name; } }
如果单例对象使用频繁,那么饿汉模式可能是一个不错的选择。类加载过程便已经完成了单例的实例化,在之后的调用过程中,无需再进行实例化,也无需害怕因为线程同步导致的性能损耗。如果你的单例对象占用较多资源,并且调用频率较低,则可考虑使用静态内部类,如果涉及到反序列化创建对象时可尝试使用枚举的方式来实现单例。
- 适用场景
- 当类只能有一个实例。
- 当这个唯一实例是通过子类化可扩展的,并且客户应该无需更改代码就能使用一个扩展的实例时。
- 优点
- 对唯一实例的受控访问:因为Singleton类封装它的唯一实例,所以它可以严格的控制客户怎样以及何时访问它。
- 缩小命名空间:Singleton模式是对全局变量的一种改进。它避免了那些存储唯一实例的全局变量污染命名空间。
- 允许对操作和表示的精化:Singleton类可以有子类,而且用这个扩展类的实例来配置一个应用是很容易的。你可以用你所需要的类的实例在运行时刻配置应用。
- 允许可变数目的实例:这个模式使得你易于改变你的想法,并允许Singleton类的多个实例。此外,你可以用相同的方法来控制应用所使用的实例的数目。只有允许访问Singleton实例的操作需要改变。
- 比类操作更灵活:一种封装单例功能的方式是使用类操作(即静态成员函数),但这难以改变设计以允许一个类有多个实例。
- 相关模式
Abstract Factory、Builder和Prototype模式都可以使用Singleton模式实现。
- 参考文章