在日常开发中,单例模式是我们计较常用的设计模式之一,今天,我们就来看一下单例模式的几种实现。
定义
确保一个类只有一个实例,并提供一个全局访问点来访问这个唯一的实例
单例模式是一种对象创建型模式。
单例模式3要素
- 声明一个类型为自身的静态私有成员变量
- 声明一个公有的静态工厂方法,返回唯一实例
- 将构造函数的可见性修改为private
饿汉式单例
饿汉式单例的实例在类装载时进行创建。由于是在类装时候创建, 所以能够保证线程安全,而且反应时间和调用速度要优于懒汉式单例。
实现代码:
public class EagerSingleton {
private static EagerSingleton eagerSingleton = new EagerSingleton();
private EagerSingleton() {
}
private static EagerSingleton getInstance() {
return eagerSingleton;
}
}
饿汉式单例存在2个问题,如下所示:
- 类装载时该对象就被创建,如果此对象没有被调用,那么会造成资源的浪费
- 对象创建时会调用该类的构造方法,如果构造方法中有过多处理会导致该类加载时间比较长
懒汉式单例与双重校验锁(Double Check Lock)
懒汉式单例的实例在第一次被引用时初始化,实现了延迟加载。
懒汉式单例存在一个很严重的问题:线程不安全。如果在高并发、多线程环境下实现懒汉式单例的话,可能会创建多个实例对象,为了解决这个问题,一般在实现懒汉式单例时会加同步锁,代码如下:
public class LazySingleton {
private static LazySingleton lazySingleton;
private LazySingleton() {
}
private static LazySingleton getInstance() {
//第一重判断,避免非必要加锁,判断实例是否存在,不存在则加锁创建
if (lazySingleton == null) {
//上锁,某一时刻只允许一个线程访问
synchronized (LazySingleton.class) {
//第二重判断
if (lazySingleton == null) {
lazySingleton = new LazySingleton();
}
}
}
return lazySingleton;
}
}
有的同学可能会说直接使用synchronized修改getInstance()方法不就可以了吗,不用这么麻烦。是,直接修饰方法确实可以达到效果,但是众所周知,synchronized效率低,而且只有当其修饰的代码块执行完成后才会释放锁,因此synchronized修饰的代码能少则少。
那么,使用了DCL是否就能百分百确保线程安全了吗?答案是否定的,因为jvm存在乱序执行功能,分析如下:
lazySingleton = new LazySingleton();
执行上面创建语句时,jvm分3步:
1. 在堆内存开辟内存空间
2. 在堆内存中实例化LazySingleton里面的各个参数
3. 把对象指向堆内存空间
由于jvm存在乱序执行功能,所以可能在2还没执行时就先执行了3,如果此时再被切换到线程B上,由于执行了3,lazySingleton 已经非空了,会被直接拿出来用,这样的话,就会出现异常。这个就是著名的DCL失效问题。那么这个问题如何解决呢?在JDK1.6及以后,只要使用volatile修饰该参数即可,因为volatile确保顺序正常执行。如下所示:
private volatile static LazySingleton lazySingleton;
静态内部类单例
外部类加载时并不需要立即加载内部类,内部类(不论是静态内部类还是非静态内部类)都是在第一次使用时才会被加载,内部类不被加载则不去初始化INSTANCE,因此静态内部类单例的实例在第一次被引用时初始化。
实现了延迟加载,并且线程安全。
那么静态内部类是如何保证线程安全的呢?可查看此篇文章JAVA类加载过程&主动引用和被动引用
代码实现:
public class StaticInnerClassSingleton {
private StaticInnerClassSingleton() {
}
public static StaticInnerClassSingleton getInstance() {
return SingletonHolder.INSTANCE;
}
static class SingletonHolder{
static StaticInnerClassSingleton INSTANCE = new StaticInnerClassSingleton();
}
}
静态内部类有着一个致命的缺点,就是传参的问题,由于是静态内部类的形式去创建单例的,故外部无法传递参数进去,例如Context这种参数。
枚举单例
以上几种单例模式都有一个通病,就是无法防止反射机制的漏洞,从而无法保证对象的唯一性。那如何解决此问题呢?枚举单例就为此而生,枚举单例代码如下:
public enum EnumSingleton{
INSTANCE;
}
枚举单例也有一个问题,那就是无法实现懒加载(延迟加载)。所以,我们创建单例时,请结合自己的使用场景选择不同的方式。
总结
单例模式优点:
-
可以确保对象的唯一性,并提供了对唯一实例的受控访问。因为单例类封装了它的唯一实例,所以可以严格控制用户怎么访问它以及何时访问它
-
可以节约系统资源,因为在系统中只存在一个对象。对于一些需要频繁创建和销毁的对象,使用单例可以提高系统的性能
单例模式缺点:
-
由于单例模式中没有抽象层,因此,单例类的扩展有很大困难
-
单例类的职责过重,在一定程度上违背了单一职责原则。 因为单例类即提供了业务方法,也提供了创建对象的方法。