先给出代码:
class Cat {
private String name;
//volatile 能够防止指令重排造成的问题
private volatile static Cat cat; //声明唯一实例
private Cat(String name) {
this.name = name;
}
public static Cat getInstance() {
if (cat == null) {
//保证加锁之前只有一个对象
synchronized (Cat.class) {
//获取锁
if (cat == null) {
cat = new Cat("DCL懒汉猫");
}
}
}
return cat;
}
}
单例模式的特点:
- 单例类只能有一个实例对象;
- 单例类必须自己创建这个实例;
- 单例类必须提供获取之一实例的方法:getInstance()。
两次判断对象是否为 null 的目的分别是什么?
最外层的 if 判断很好理解嘛,如果对象已经不为空了,已经创建过了,那您就甭费劲抢锁了,直接拿着实例返回吧。
那为什么在抢到锁之后还要再加一层判断呢?大家来考虑这样一种情况:
两个线程 A 和 B 来获取对象,此时对象还没被创建,A 和 B 都通过了第一层判断,两个线程开始竞争锁,A 抢到了锁,因此 B 进入同步队列阻塞等待。**当 A 创建完对象并且释放了锁,B 拿到了锁,又去执行 new Cat(“DCL懒汉猫”),此时就创建了两个对象。**因此,必须在加锁之后再来一次判断,才能保证只创建一次对象。
为什么要给实例对象添加 volatile 关键字呢?
这是因为编译器在编译时会进行指令重排,而 volatile 可以禁止指令重排。
对象的创建大概有这么三个步骤(从指令层面来看啊,从JVM来讲具体还有很多步骤):
- 分配内存空间
- 执行构造方法,初始化对象
- 把这个对象指向这个空间
正常是按 123 的顺序执行,但由于指令重排的存在,可能会存在 A 线程按照132 执行,当执行到 3 时,线程 B 来取对象,就会得到空值。因此必须给单例对象加上 volatile 关键字。
最后
其实DCL懒汉式在 Java 里面也不是绝对安全的,可以通过反射区去破坏它。可以通过枚举类创建绝对安全的单例模式,枚举类不仅能防止反射,还能防止反序列化去创建对象。