延迟加载
延迟加载是指等到真正使用时去创建实例,不使用时不创建实例
对比延迟加载(懒汉式)和非延迟加载(饿汉式):
- 从速度和反应时间来看,饿汉式好
- 从资源利用效率来看,懒汉式好
设计方式
非延迟加载
- 非延迟加载(饿汉)
public class SingleTon {
private static SingleTon instance = new SingleTon(); //注意是private
private SingleTon() {//私有构造方法 防止直接new对象
}
public static SingleTon getInstance() {
return instance;
}
}
延迟加载
忽略常规的延迟加载,因为存在线程问题,一般开发过程中不会用到
- 同步延迟加载
public class SingleTon {
private static SingleTon instance = null;
private SingleTon() {//私有构造方法 防止直接new对象
}
public static synchronized SingleTon getInstance() {
if(instance==null){
instance=new SingleTon();
}
return instance;
}
}
- 双重检测同步延迟加载
public class SingleTon {
private static SingleTon instance = null;
private SingleTon() {//私有构造方法 防止直接new对象
}
public static SingleTon getInstance() {
if (instance==null){
synchronized (SingleTon.class){
if(instance==null){ //此处再做一次判断是防止线程1验证过此条件new SingleTon(),此时线程2也验证过此条件,就会再次new SingleTon()
instance=new SingleTon();
}
}
}
return instance;
}
}
对比同步延迟加载,对instance进行二次检查,目的是为了避免过多的同步
然而上述方式还是存在一个问题:
大家应该了解过JVM的指令重排,没了解过的可见:有关并发编程
Java中类似于instance = new Singleton
会被编译期编译成JVM指令:
memory =allocate(); //1:分配对象的内存空间
ctorInstance(memory); //2:初始化对象
instance =memory; //3:设置instance指向刚分配的内存地址
经过JVM和CPU的优化,有可能指令重排成下面的顺序:
memory =allocate(); //1:分配对象的内存空间
instance =memory; //3:设置instance指向刚分配的内存地址 此时instance已经不再指向null
ctorInstance(memory); //2:初始化对象
那么问题就来了:
假设线程1已经执行了1,3,此时instance已经不为null,线程2正好执行到if(instance==null)
的判断,那么线程2就会直接返回一个没有经过初始化的instance,这样显然是有问题的
怎么解决这个问题?
volatile 有关volatile
public class SingleTon {
private volatile static SingleTon instance = null; //volatile 阻止JVM对instance的相关操作进行指令重排
private SingleTon() {//私有构造方法 防止直接new对象
}
public static SingleTon getInstance() {
if (instance==null){
synchronized (SingleTon.class){
if(instance==null){ //此处再做一次判断是防止线程1验证过此条件new SingleTon(),此时线程2也验证过此条件,就会再次new SingleTon()
instance=new SingleTon();
}
}
}
return instance;
}
}
- 静态内部类实现延迟加载
此种方法真正意义上实现了延迟加载
public class Singleton {
private static class LazyHolder {
private static final Singleton INSTANCE = new Singleton(); //这里private没有什么实际意义
}
private Singleton (){}
public static Singleton getInstance() {
return LazyHolder.INSTANCE;
}
}
注意点:
1.从外部无法访问静态内部类LazyHolder
,只有当调用Singleton.getInstance
方法的时候,才能得到单例对象INSTANCE
2.INSTANCE
对象初始化的时机并不是在单例类Singleton
被加载的时候,而是在调用getInstance
方法,使得静态内部类LazyHolder
被加载的时候。因此这种实现方式是利用classloader
的加载机制来实现懒加载,并保证构建单例的线程安全
然而到这里,以上的所有的方式都不是最完美的,因为Java中反射(简直就是isis)的存在
- 枚举
这种方式应该属于非延迟性加载
public enum Singleton {
INSTANCE;
public void whateverMethod() {
}
}
JVM
禁止获取枚举的私有构造方法,这种方法可以完美解决反射带来的风险,不过个人感觉开发中并不需要刻意使用这种方式
补充:
使用枚举实现的单例模式,不但可以防止利用反射强行构建单例对象,而且可以在枚举类对象被反序列化的时候,保证反序列的返回结果是同一对象
对于其他方式实现的单例模式,如果既想要做到可序列化,又想要反序列化为同一对象,则必须实现readResolve
方法
小结
实现方式 | 线程安全 | 懒加载 | 禁止反射构建 |
---|---|---|---|
双重锁检测 | 是 | 是 | 否 |
静态内部类 | 是 | 是 | 否 |
枚举 | 是 | 否 | 是 |
网友评论