########### 20190617更新
DCL缺点
1. 对象init有了内存地址,object != null,但是初始化还没完全,有属性没有被初始化:object.attribute == null,这样调用object.attribute就null point了。(声明为volatile可以解决)
2. 变得复杂(volatile,两次判空),耗时(用了synchronized 影响性能)
那好的实现方式是什么呢?优雅 优雅decent
采用静态内部类,
public class Singleton {
private Singleton(){}
public static getInstance() {
return SingletonHolder.INSTANCE;
}
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton()
}
}
1. 懒加载:静态内部类和外部类没啥关系,所以调用外部类不生成 单例对象,必须主动调用。
2. 一个对象:确保由JVM内部机制 初始化后确保static的字段
3. 没有多线程相关的synchronized、volatile,仅仅右JVM内部实现
完美优雅
############
为何使用DCL
早起JVM性能上有待优化,延迟初始化可以避免高开销(用时再初始化),或降低程序启动时间。延迟初始化需要使用同步,但同步执行的速度慢。所以就先用 不同步来判断(一次为null,就new,所以之后基本不为null),这样大多数不为null 情况下就不耗时了。等new的时候,要保证原子性,所以再加锁。
public static Singleton getInstance()
{
if (instance == null)
{
synchronized(Singleton.class) { //1
if (instance == null) //2
instance = new Singleton(); //3
}
}
return instance;
}
问题
1.可见性
线程安全:原子性、可见性。 保证了原子性,却没有考虑可见性。
可见性简单说:
a线程更改对象y----》更改CPU缓存1中的y----》写入内存中的y
b线程读取对象y----》读取CPU缓存2中的y (如果CPU缓存2没有从内存中更新y,b线程y看到过时的数据)
- instance对象 被a线程 release置空,instance == null,写入内存。
- b线程没从内存更新,所以instance != null,b线程用过时的对象 运行,导致状态不一致。
2.部分初始化
instance 指向了对象的内存地址(即instance != null),但instance没调用构造函数,所以其属性是失效的状态。
new对象cpu的指令是多步的
- 分配内存
- 初始化对象(内存赋值)
- 内存地址赋给instance (instance != null)
但CPU指令可能被优化重排序,3跑到2前面了。
假设乱序后
- a线程到3后(之后调2),CPU 调度b线程,
- b线程判断 instance != null,调用instance.hello.toString(),
- instance没调用构造函数,hello为null,直接崩溃了。
解决:
将单例对象声明为volatile类型,修复可见性、排序(参看volatile优缺点)
为何废弃DCL:
DCL产生的原因(无竞争同步的执行速度很慢,以及JVM启动很慢)已经不复存在,因而不是高效的优化措施。
参考
《Java并发编程实战》286页