简单了解几种单例模式
说到单例模式就有必要理解一下什么是单例模式:
单例模式是23中设计模式中较为常见的一种设计模式,其类别归属为创建型设计模式。单例可以简单定义为一个类只能产生一个示例,即所谓的全局唯一。
实现几种单例模式的方式大同小异,他们共同的核心步骤为:
1. 将类的构造方法私有化,即`private Singleton() {}`,这样做的目的是防止代码段中通过构造函数生成不同的实例对象。
2. 在类中提供一个静态方式,旨在生成一个唯一的实例,如果静态引用为空则尝试初始化一个对象,并将引用指向它,否则直接返回。
饿汉式和懒汉式
/**
* 饿汉式单例
*
* @author chenq
*/
public class SingletonHungry {
// 主动创建实例对象
private static SingletonHungry singletonHungry = new SingletonHungry();
// 构造方法私有化
private SingletonHungry() {
}
// 静态共有方式,获取类实例唯一路径
public static SingletonHungry getInstance() {
return singletonHungry;
}
}
众所周知,类加载的方式是按需加载,且加载一次,因此上述类在加载时就会生成一个SingletonHungry对象,即在整个生命周期只会创建一次对象,充分保证单例。
它主要的问题是存在内存浪费,甚至可能导致内存泄漏。不管代码是否使用,在类加载时就创建类实例,没有达到延迟加载的效果,也无意义的占用了内存空间。
/**
* 懒汉式单例
*
* @author chenq
*/
public class SingletonLazy {
// 实例引用
private static SingletonLazy singletonLazy;
// 构造方法私有化
private SingletonLazy() {
}
// 静态共有方式,获取类实例唯一路径
public static SingletonLazy getInstance() {
// 当需要时才创建实例
if (singletonLazy == null) {
singletonLazy = new SingletonLazy();
}
return singletonLazy;
}
}
懒汉式的单例可以看到是延迟加载的,只有第一次调用getInstance方式才会创建实例。
它的问题是只能在单线程下使用,多线程的情况下会出现创建多个实例的情况,这在单例模式中是不允许的,所有才出现了本文所探讨的双重校验锁单例模式。
双重校验锁实现单例模式
/**
* 双重校验锁实现单例
*
* @author chenq
*/
public class SingletonDoubleCheck {
// 类引用
private static volatile SingletonDoubleCheck singletonDoubleCheck;
// 构造函数私有化
private SingletonDoubleCheck() {
}
// 双重校验 + 锁实现单例
public static SingletonDoubleCheck getInstance() {
// 第一次校验是否为null
if (singletonDoubleCheck == null) {
// 不为空则加锁
synchronized (SingletonDoubleCheck.class) {
// 第二次校验是否为null
if (singletonDoubleCheck == null) {
singletonDoubleCheck = new SingletonDoubleCheck();
}
}
}
return singletonDoubleCheck;
}
}
第一次校验是否为null:
主要是为了实现返回单例,避免多余的加锁操作,以及锁的等待和竞争,如果条件不成立就说明已经生成实例,直接返回即可,提高程序执行的效率。
第二次校验是否为null:
第二次校验是关键,这里防止了多线程创建多个实例(一般为两个),这里的特殊情况是这样的:在未创建实例的情况下,A线程和B线程都通过了第一次校验(singletonDoubleCheck为空),这时如果通过竞争B线程拿到了锁就会执行一次new操作,生成一个实例,然后B执行完了A就会拿到资源的锁,如果没有第二次判断的话,这时A线程也会执行一次new操作,这里就出现了第二个类实例,违背了单例原则。所以说两次校验都是必不可少的。
提一下上述代码中类引用中的
volatile关键字是不能少的:常见的,该关键字能够实现变量在内存中的可见性(告诉JVM在使用该关键字修饰的变量时在内存中取值,而不是用特定内存区域的副本,因为真实的值可能已经被修改过了),它的另外一种作用是防止JVM对指令进行重排。
其实,在
new一个对象的时候会有如下步骤(指令):1. 分配内存空间 2. 初始化引用 3. 将引用指向内存空间正常的逻辑肯定以为是这样执行的 1 -> 2 -> 3,但是偏偏JVM拥有指令重排的能力,所以说执行顺序是随机的,可能是 1 -> 3 -> 2,这样的话在多线程环境下可能会拿到空引用:线程A先执行了1,3步骤,紧接着线程B执行
getInstance,发现不为null(这里的==是判断实际的值,即引用指向的内存空间),就会返回引用,然而此时引用未初始化。所以说volatile在这里保证指令的执行顺序,在多线程情况下不可少。