一、意图与动机
保证一个类仅有一个实例,并且提供一个全局访问点。
二、效果
1、对唯一实例的受控访问。
2、缩小命名空间。单例模式是对全局变量的一种改进,避免污染全局变量空间
3、允许可变数目的实例。可以改进单例模式,精确控制实例个数
三、实现
1、线程不安全的实现(lazy init)
package com.yangyi.singleton;
/**
* Created by yangjinfeng02 on 2016/9/27.
*/
public class Singleton1 {
private static Singleton1 INSTANCE;
private Singleton1(){}
public static Singleton1 Instance() {
if (INSTANCE == null) {
INSTANCE = new Singleton1();
}
return INSTANCE;
}
}
缺陷:多线程下不可用,无法确保只创建一个实例
2、同步方法(lazy init)
针对1中多线程不可用的问题,最直观手段是将访问方法做同步处理,从而实现线程安全的延迟加载
package com.yangyi.singleton;
/**
* 同步处理,线程安全,延迟加载
* Created by yangjinfeng02 on 2016/9/27.
*/
public class Singleton2 {
private static Singleton2 INSTANCE;
private Singleton2(){}
public static synchronized Singleton2 Instance() {
if (INSTANCE == null) {
INSTANCE = new Singleton2();
}
return INSTANCE;
}
}
由于对Instance()方法做了同步处理,能够保证多线程可用,但synchronized将导致性能开销,如果多个线程频繁调用该方法,将会导致程序执行性能的下降。反之,如果该方法调用不频繁,这种实现则是可以使用的。
3、双重检查锁定(lazy init)——不正确的一种实现
为了解决2中同步带来的性能开销,一种思路是双重锁检查,只在第一次创建的时候做同步操作
package com.yangyi.singleton;
/**
* 双重锁检查,线程不安全,延迟加载
* Created by yangjinfeng02 on 2016/9/27.
*/
public class Singleton3 {
private static Singleton3 INSTANCE;
private Singleton3(){}
public static Singleton3 Instance() {
// 第一次检查
if (INSTANCE == null) {
// 枷锁
synchronized (Singleton3.class) {
// 第二次检查
if (INSTANCE == null) {
// 创建
INSTANCE = new Singleton3();
}
}
}
return INSTANCE;
}
}
这种实现看起来比较完美,但这是一个错误的优化!
错误的地方:上述代码中第一次判断INSTANCE不为null,读取到的对象可能尚未初始化完毕。
问题的根源
代码中new创建实例的那一行,在jvm中实际做的事情大致可以归结为三点:
(1)、分配对象内存
(2)、初始化对象
(3)、设置INSTANCE指向分配的内存地址
而这3点中,2和3是可能发生重排序的,如果2和3的顺序被重排后,那么执行过程如下
(1)、分配对象内存
(3)、设置INSTANCE指向分配的内存地址
(2)、初始化对象
之所以会发生这种重排序,是因为java语言规范指定,所有线程在执行程序的时候必须遵循intra-thread semantics. intra-thread semantics保证重排序不会改变单线程内的执行结果,换句话说,intra-thread semantics允许那些单线程内不会改变程序执行结果的重排序。
接着来看这种重排序会导致的后果,如果线程A完成了(1)和(3),此时线程B去判断INSTANCE是否为null,此时INSTANCE不为null,但是所指向的内存地址空间尚未初始化完毕,所以此时线程B取到的实例是并未初始化完成的。
解决方案
知晓了问题的根源,我们来看如何解决,有两个思路:
方案一、不允许(2)和(3)重排序
方案二、允许(2)和(3)重排序,但是不允许其它线程看到这个重排序
根据这两种思路,于是有下面的两种实现方案
4、基于volatile的方案
我们知道java中的volatile关键字的作用就是限制指令的重排序,所以相对于3,我们只需要做一个小小的改动就可以将3中的方案变为正确无误、线程安全的实现方式。我们只需要将INSTANCE声明为volatile即可
package com.yangyi.singleton;
/**
* 基于volatile的双重锁检查,线程安全,延迟加载
* Created by yangjinfeng02 on 2016/9/27.
*/
public class Singleton4 {
private volatile static Singleton4 INSTANCE;
private Singleton4(){}
public static Singleton4 Instance() {
// 第一次检查
if (INSTANCE == null) {
// 枷锁
synchronized (Singleton4.class) {
// 第二次检查
if (INSTANCE == null) {
// 创建
INSTANCE = new Singleton4();
}
}
}
return INSTANCE;
}
}
5、基于JVM类初始化的解决方案
jvm在类的初始化阶段(即在class被加载后,且被线程使用之前),会执行类的初始化,在初始化期间,JVM会获取一个锁,这个锁可以同步多个线程对同一个类的初始化。
利用这个特性,我们可以实现另外一种线程安全的单例模式
package com.yangyi.singleton;
/**
* 基于jvm类加载机制的单例实现
* Created by yangjinfeng02 on 2016/9/27.
*/
public class InstanceFactory {
private static class InstanceHolder {
public static Instance INSTANCE = new Instance();
}
public static Instance Instance() {
// 此处将触发InstanceHolder类的初始化
return InstanceHolder.INSTANCE;
}
}
另外一种更加简洁的写法,利用静态常量
package com.yangyi.singleton;
/**
* Created by yangjinfeng02 on 2016/9/28.
*/
public class Singleton5 {
private static final Singleton5 INSTANCE = new Singleton5();
private Singleton5() {};
public static Singleton5 getInstance() {
return INSTANCE;
}
}
另外一种实现方式,通过静态代码块
package com.yangyi.singleton;
/**
* Created by yangjinfeng02 on 2016/9/28.
*/
public class Singleton6 {
private static Singleton6 INSTANCE;
private Singleton6() {};
static {
INSTANCE = new Singleton6();
}
public static Singleton6 getInstance() {
return INSTANCE;
}
}
上述三种实现均是基于jvm类初始化的解决方案,没有禁止指令重排,但是通过jvm类初始化时候的锁,将指令重排对第二个线程不可见
6、基于enum的实现方式
在effective java中介绍了一种基于enum实现单例的简洁方法
package com.yangyi.singleton;
/**
* Created by yangjinfeng02 on 2016/9/28.
*/
public enum Singleton7 {
INSTANCE;
public void leaveTheBuilding() {
}
}
四、总结
如此多的实现方案,思路不一样,但是目的是一致的。实际使用中可更具场景选择合适的方案,整体来说,基于类初始化的方案比较受推荐。