单例模式就是保证对象只被创建一次,并在整个程序中复用。
主要被用于一个全局类的对象在多个地方被使用并且对象的状态是全局变化的场景下。
单例模式保障了整个系统只有一个对象能被使用,很好地节约了资源。
饿汉模式就是在加载类(比如下面加载Singleton)的时候就直接new一个对象,然后在方法中直接将对象返回给用户
public class Singleton {
// 使用static修饰,类加载的时候new一个对象
private static Singleton INSTANCE = new Singleton();
// 构造器私有化
private Singleton() {}
public static Singleton getInstance() {
return INSTANCE;
}
}
懒汉模式就是在加载类的时候只声明变量,不new对象,后面需要用到的时候再new对象,并把对象赋给变量
public class Singleton {
private static Singleton INSTANCE;
// 构造器私有化
private Singleton() {}
public static Singleton getInstance() {
if (INSTANCE == null) {
INSTANCE = new Singleton();
}
return INSTANCE;
}
}
饿汉模式在getInstance方法调用前实例已经被创建,因此实例在类加载的时候就已经存在于JVM中,因此饿汉模式是线程安全的,而懒汉模式则是在调用getInstance方法后才创建实例,因此线程是不安全的
通过在类中定义一个静态内部类,将对象实例的创建与初始化放在内部类中完成,我们在getInstance中获取对象直接通过静态内部类调用单例对象
正是因为类的静态内部类在JVM中的唯一性才保证了单例对象的唯一性,从而静态内部类同样是线程安全的
public class Singleton {
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton(){}
public static final Singleton getInstance(){
return SingletonHolder.INSTANCE;
}
}
普通的懒汉模式在单线程场景下是线程安全的,但在多线程场景下是非线程安全的。
先来看看普通的懒汉模式
public class Singleton {
private static Singleton INSTANCE;
private Singleton() {}
public static Singleton getInstance() {
if (INSTANCE == null) {
INSTANCE = new Singleton();
}
return INSTANCE;
}
}
在多线程同时调用getInstance方法时,由于方法没有加锁,可能会出现以下情况:
对于1的解释
public static Singleton getInstance() {
if (INSTANCE == null) {
/**
* 由于没有加锁,当线程A刚执行完if判断INSTANCE为null后还没来得及执行INSTANCE = new Singleton()
* 此时线程B进来,if判断后INSTANCE为null,且执行完INSTANCE = new Singleton()
* 然后,线程A接着执行,由于之前if判断INSTANCE为null,于是执行INSTANCE = new Singleton()重复创建了对象
*/
INSTANCE = new Singleton();
}
return INSTANCE;
}
对于2的解释
public static Singleton getInstance() {
if (INSTANCE == null) {
/**
* 由于没有加锁,当线程A刚执行完if判断INSTANCE为null后开始执行 INSTANCE = new Singleton()
* 但是注意,new Singleton()这个操作在JVM层面不是一个原子操作
*
*(具体由三步组成:1.为INSTANCE分配内存空间;2.初始化INSTANCE;3.将INSTANCE指向分配的内存空间,
* 且这三步在JVM层面有可能发生指令重排,导致实际执行顺序可能为1-3-2)
*
* 因为new操作不是原子化操作,因此,可能会出现线程A执行new Singleton()时发生指令重排的情况,
* 导致实际执行顺序变为1-3-2,当执行完1-3还没来及执行2时(虽然还没执行2,但是对象的引用已经有了,
* 只不过引用的是一个还没初始化的对象),此时线程B进来进行if判断后INSTANCE不为null,
* 然后直接把线程A new到一半的对象返回了
*/
INSTANCE = new Singleton();
}
return INSTANCE;
}
因此通过单例模式的双重检验锁模式去解决以上问题
public class Lock2Singleton {
private volatile static Lock2Singleton INSTANCE; // 加 volatile
private Lock2Singleton() {}
public static Lock2Singleton getSingleton() {
if (INSTANCE == null) { // 双重校验:第一次校验
synchronized(Lock2Singleton.class) { // 加 synchronized
if (INSTANCE == null) { // 双重校验:第二次校验
INSTANCE = new Lock2Singleton();
}
}
}
return INSTANCE;
}
}
为啥要双重校验?
第一次校验是为了提高效率,避免INSTANCE不为null时仍然去竞争锁
第二次校验是为了避免多个线程重复创建对象
为啥要加volatitle?
加volatitle是为了禁止指令重排,避免某个线程可能会得到一个未完全初始化的对象
具体执行过程: