全局只存在唯一一个对象,该类负责创建自己的对象,同时确保只有一个对象被创建,分别为懒汉式、饿汉式、双重检查锁模式。
- 单例类只能有一个实例(构造方法私有)
- 单例类必须自己创建自己的唯一实例(类中有方法来创建对象)
- 单例类必须给所有其他对象提供这一实例(必须提供访问单例对象的方法)
饿汉式(线程安全)
这种方式在类加载时就完成了初始化,导致类加载的速度变慢,但是这种方式天生就是线程安全的。
public class Demo1 {
private static Demo1 demo1 = new Demo1();
private Demo1() {
}
public Demo1 getInstance() {
return demo1;
}
}
懒汉式(线程不安全)
这种方式虽然延迟了初始化的过程,但在多线程的情景下是不安全的。
public class Demo1 {
private static Demo1 demo1;
private Demo1() {
}
public static Demo1 getInstance() {
if (demo1 == null) {
demo1 = new Demo1();
}
return demo1;
}
}
DCL双重检查锁模式(线程安全)
线程安全,延迟初始化。这种方式采用双锁机制,安全且在多线程情况下能保持高性能。
1、为什么要进行第一次判空?
在多线程并发的时候,如果不判断空,每个线程都要抢占synchronized锁,而且每次都只有一个线程才能抢占到锁,其他线程要等待它释放锁之后才可以执行。所以第一次判空是为了提高程序的效率。
2、为什么要进行第二次判断空?
如果没有第二次判空,可能线程A和线程B都进到了第一个if块中,此时线程A抢到了锁先执行,线程B等待A执行完成释放锁之后,又可以拿到锁继续创建一个新的对象,这就违反了我们的原则。所以要有第二次判空。
3、为什么变量要加volatile关键字?
为了避免指令重排序,demo1 = new Demo1在我们看来就是一句话而以,可是在虚拟机中,它分为3个指令动作:
- 为对象分配空间
- 初始化对象
- 将引用指向对象的内存空间地址
如果在线程A中执行demo1 = new Demo1是按照132顺序执行,执行到3的时候,demo1已经不为空了。如果此时线程B到达第一个if时,此时demo1 != null,则会直接返回一个半初始化的demo1对象,这时就会产生bug。
public class Demo1 {
private static Demo1 demo1;
private Demo1() {
}
public static Demo1 getInstance() {
if (demo1 == null) {
synchronized (Demo1.class) {
if (demo1 == null) {
demo1 = new Demo1();
}
}
}
return demo1;
}
}