一、单例模式概览
在《设计模式之禅》中提到了单例模式的定义如下:Ensure a class has only one instance, and provide a global point of access to it. (确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。
Singleton类称为单例类,通过使用private的构造函数确保了在一个应用中只产生一个实例,并且是自动实例化的(在Singleton中自己使用new Singleton() )。
细心留意会发现,单例模式在我们的日常开发中并不少见。例如:
-
数据库的连接池不会反复创建
-
spring中一个单例模式bean的生成和使用
-
在我们平常的代码中需要设置全局的一些属性保存
可见单例模式虽然简单,但却有着较广的使用面。同时单例模式也有一些细节值得我们注意,下面我们通过7种单例模式的写法来一一探讨这些细节。
二、7种单例模式的实现
1. 懒汉式,线程不安全
public class Singleton {
private static Singleton instance;
private Singleton(){}
public static Singleton getInstance(){
if(instance == null){
return new Singleton();
}
return instace
}
}
这段代码就是最基本的懒汉式单例模式实现。然而在多线程的环境下,会有多个请求同时调用getInstance( )方法,从而创建多个实例,造成资源浪费,也违背了单例的原则。
2. 懒汉式,线程安全
public static sychronized Singleton getInstance(){
if(instance == null){
return new Singleton();
}
return instace
}
为了解决上面的问题,最简单的方法就是将整个 getInstance() 方法设为同步(synchronized)。虽然做到了线程安全,并且解决了多实例的问题,但是它并不高效。因为在任何时候只能有一个线程调用 getInstance() 方法。但是同步操作只需要在第一次调用时才被需要,即第一次创建单例实例对象时。这就引出了双重检验锁。
3. 双重校验锁
public static Singleton getSingleton() {
if(instance == null){ //Single Checked
sychronized(Singleton.class){
if(instance == null){ //Single Checked
instance = new Singleton()
}
}
}
}
双重检验锁模式(double checked locking pattern),是一种使用同步块加锁的方法。通常称其为双重检查锁,因为会有两次检查 instance == null
,一次是在同步块外,一次是在同步块内。为什么在同步块内还要再检验一次?因为可能会有多个线程一起进入同步块外的 if,如果在同步块内不进行二次检验的话就会生成多个实例了。
这段代码看起来很完美,很可惜,它是有问题。主要在于instance = new Singleton()
这句,这并非是一个原子操作,事实上在 JVM 中这句话大概做了下面 3 件事情。
- 给 instance 分配内存
- 调用 Singleton 的构造函数来初始化成员变量
- 将instance对象指向分配的内存空间(执行完这步 instance 就为非 null 了)
但是在 JVM 的即时编译器中存在指令重排序的优化。也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是后者,则在 3 执行完毕、2 未执行之前,被线程二抢占了,这时 instance 已经是非 null 了(但却没有初始化),所以线程二会直接返回 instance,然后使用,然后顺理成章地报错。
为了解决这个问题,我们需要将instance变量声明成volatile。
public 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;
}
}
这里使用volatile能保证多线程状态不报错的主要原因是:volatile禁止指令重排序优化的特性。在 volatile 变量的赋值操作后面会有一个内存屏障(生成的汇编代码上),读操作不会被重排序到内存屏障之前。比如上面的例子,取操作必须在执行完 1-2-3 之后或者 1-3-2 之后,不存在执行到 1-3 然后取到值的情况。从「先行发生原则」的角度理解的话,就是对于一个 volatile 变量的写操作都先行发生于后面对这个变量的读操作(这里的“后面”是时间上的先后顺序)。
到这里,我们就写出了一个相对完美的懒汉式单例,不过我们也可以使用更多其他的实现方式。
4. 饿汉式 static final field
public class Singleton{
// 类加载时就初始化
private static final Singleton instance = new Singleton();
private Singleton(){}
public static Singleton getInstance(){
return instance;
}
}
这种写法将单例的实例声明成final和static,在第一次加载类到内存中就会被初始化,是线程安全的。但因为它不是懒加载的,如果我们在后续的编程中并没有使用到它就会存在资源浪费。饿汉式的创建方式在一些场景中将无法使用:譬如 Singleton 实例的创建是依赖参数或者配置文件的,在 getInstance() 之前必须调用某个方法设置参数给它,那样这种单例写法就无法使用了。
5. 静态内部类
public class Singleton{
private static class SingletonHolder{
private static final Singleton INSTANCE = new Singleton();
}
private Singleton(){}
public static final Singleton getInstace(){
return SingletonHolder.INSTANCE;
}
}
这种写法仍然使用JVM本身机制保证了线程安全问题;由于 SingletonHolder 是私有的,除了 getInstance() 之外没有办法访问它,因此它是懒汉式的;同时读取实例的时候不会进行同步,没有性能缺陷。
6. CAS 线程安全
public class Singleton{
private static final AtomicReference<Singleton> INSTANCE = new AtomicReference<Singleton>();
private static Singleton instance;
public static final Singleton getInstance(){
for(;;){
Singleton instance = INSTANCE.get();
if(instance !=null){
INSTANCE.compareAndSet(null,new Singleton());
}
return INSTANCE.get();
}
}
}
java并发库提供了很多原子类来支持并发访问的数据安全性;AtomicInteger
、AtomicBoolean
、AtomicLong
、AtomicReference
。AtomicReference 可以封装引用一个实例,支持并发访问如上的单例方式就是使用了这样的一个特点。
使用CAS的好处就是不需要使用传统的加锁方式保证线程安全,而是依赖于CAS的忙等算法,依赖于底层硬件的实现,来保证线程安全。相对于其他锁的实现没有线程的切换和阻塞也就没有了额外的开销,并且可以支持较大的并发性。
当然CAS也有一个缺点就是忙等,如果一直没有获取到将会处于死循环中。
7. 枚举 Enum
public enum Singleton{
INSTANCE;
}
创建枚举默认就是线程安全的,所以不需要担心double checked locking,而且还能防止反序列化导致重新创建新的对象。
以上就是单例模式实现的七种方式。但严格来说前两种都存在或多或少的问题,在多线程环境下应该尽量使用后面五种方法。
三、单例模式的优缺点及使用场景总结
由于单例模式在内存中只有一个实例,减少了内存开支,特别是一个对象需要频繁地创建、销毁时,单例模式的优势就非常明显。同时,单例模式还可以避免对资源的多重占用,例如一个写文件动作,由于只有一个实例存在内存中,避免对同一个资源文件的同时写操作。除此之外,单例模式可以在系统设置全局的访问点,优化和共享资源访问,例如可以设计一个单例类,负责所有数据表的映射处理。
单例模式的缺点也很明显,那就是难以扩展,除了修改代码基本没有第二种途径可以实现。
单例模式的应用场景如下:
- 要求生成唯一的序列号
- 在整个项目中需要一个共享访问点或共享数据
- 创建一个对象需要消耗的资源过多,如要访问IO和数据库资源等
- 需要定义大量的静态常量和静态方法(如工具类)的环境
以上,就是关于单例模式的一些基本知识。如何巧妙的运用这些设计模式到我们的实际代码编写中,仍然需要我们不断的学习和感悟。