参考文章: https://blog.csdn.net/weixin_44471490/article/details/108929289
解释得很好,一步一步分析为什么这样 写
单例模式
单例即单实例,只实例出来一个对象。
一般在创建一些管理器类、工具类的时候,需要用到单例模式,比如JDBCUtil 类,我们只需要一个实例即可(多个实例也可以实现功能,但是增加了代码量且降低了性能)。
如何实现单例:
- 将构造方法私有化
- 提供一个全局唯一获取该类实例的方法帮助用户获取类的实例
应用场景:
用于全局类对象在多个地方被使用,且对象状态是全局的场景下
单例模式优点:
单例模式为系统资源的优化提供了思路, 频繁创建和销毁会增加资源消耗,单例模式保障了整个系统只有一个对象能被使用,很好节约资源
写法
- 饿汉模式
- 懒汉模式
- 静态内部类
- 双重校验锁
饿汉模式
就是在加载类的时候直接new一个对象,后面直接使用即可
饿汉模式在类中直接定义全局静态对象实例并初始化,然后提供方法获取该实例对象
public class Singleton {// 私有静态实例private static Singleton INSTANCE = new Singleton();// 构造器私有化private Singleton() {}public static Singleton getInstance() {return INSTANCE;}
}
懒汉模式
懒汉模式在加载类的时候,只声明,不new对象,后面用到的时候再new对象,然后把对象赋给该变量。
定义一个私有的静态对象INSTANCE,之所以定义INSTANCE为静态,是因为静态属性或者非法是属于类的,能够很好保证单例对象唯一性;
然后定义一个静态方法获取该对象,如果对象为null,则new一个对象并将其赋值给INSTANCE。
public class Singleton {private static Singleton INSTANCE;
// 构造器私有化private Singleton() {}public static Singleton getInstance() {if (INSTANCE == null) {INSTANCE = new Singleton();}return INSTANCE;}
}
饿汉与懒汉模式区别:
- 饿汉在类加载时实例化,在Class Loader完成后类的实例已存在于JVM中。
- 懒汉模式在类定义了变量未初始化,是理解在获取单例对象的方法中实现,即 在getInstance方法第一次调用后才创建实例,new对象操作在getInstance方法内
- 此外: 饿汉模式实例在类加载时已存在,所以说线程安全的; 懒汉模式第一次调用getInstance才实例化,该方法不是线程安全的
静态内部类
用于将对象的定义和初始化放在内部类中完成,在获取对象时要通过静态内部类调用其单例对象
之所以要这样设计,是因为类的静态内部类是JVM中唯一的,保障了单例对象的唯一性。
静态内部类单例实现是线程安全的
饿汉模式和静态内部类实现单例模式的优点是写法简单,缺点是不适合复杂对象的创建。对于涉及复杂对象创建的单例模式,比较优雅的实现方式是懒汉模式,但是懒汉模式是非线程安全的,下面就讲一下懒汉模式的升级版——双重构校验锁模式(双重构校验锁是线程安全的)。
双重校验锁
饿汉模式是不需要加锁来保证单例的,而懒汉模式虽然节省了内存,但是却需要使用锁来保证单例,因此,双重校验锁就是懒汉模式的升级版本。
单线程懒汉模式
普通的懒汉模式在单线程场景下是线程安全的,但在多线程场景下是非线程安全的。
public class Singleton {private static Singleton INSTANCE;private Singleton() {}public static Singleton getInstance() {if (INSTANCE == null) {INSTANCE = new Singleton();}return INSTANCE;}
}
单线程懒汉模式的问题
上面这段代码在单线程环境下没有问题,但是在多线程的情况下会产生线程安全问题。
在多个线程同时调用getInstance方法时,由于方法没有加锁,可能会出现以下情况
- ① 这些线程可能会创建多个对象
- ② 某个线程可能会得到一个未完全初始化的对象
对于 ① 的情况解释如下:
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;
}
对于 ② 的情况解释如下:
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;
}
解决问题:加锁
为了解决问题1, 可以对getInstance方法加锁
public class Singleton {private static Singleton INSTANCE;private Singleton() {}public static synchronized Singleton getInstance() { // 加锁if (INSTANCE == null) {INSTANCE = new Singleton();}return INSTANCE;}
}
这里粗暴地对整个getInstance方法加锁,代价很大: 只有第一次getInstance才需要同步创建对象,创建之后再次调用getInstance时就只是简单返回成员变量,但是这里无需同步,没必要对整个方法加锁
同步一个方法会减低上百倍性能,每次调用获取和释放锁开销是可以避免的。
初始化完成后,锁就不应该留着,可以只对方法的部分代码加锁!
public class Lock2Singleton{private static Lock2Singleton INSTANCE;private Lock2Singleton() {}public static Lock2Singleton getSingleton() {synchronized(Lock2Singleton.class) {if(INSTANCE == null) {INSTANCE = new Lock2Singleton();}}return INSTANCE;}
}
优化后对代码 if(INSTANCE == nul) 和INSTANCE = new Lock2Singleton()加锁
每个线程进入方法后先加锁,保证不会有多个线程同时用于创建对象,保证唯一性
如何解决问题2?
需要禁止指令重排,加volatile关键字, 防止得到未初始化对象
public class Lock2Singleton{private volatile static Lock2Singleton INSTANCE;private Lock2Singleton() {}public static Lock2Singleton getSingleton() {synchronized(Lock2Singleton.class) {if (INSTANCE == null) {INSTANCE = new Lock2Singleton();}}}return INSTANCE;
}
从功能上是完整的,在性能上还可以优化:
在synchronized代码块上,加锁是非常影响效率的,如果INSTANCE不为null,可以先判断INSTANCE是否为null,防止进入synchronized代码块
public class Lock2Singleton{private vloatile static Lock2Singleton INSTANCE;private Lock2Singleton() {}public static Lock2Singleton getSingleton() {if (INSTANCE == null) {synchronized(Lock2Singleton.class) {if (INSTANCE == null{INSTANCE = new Lock2Singleton();}}}return INSTANCE;}
}
在 synchronized 代码块之外再加一个 if 判断,这样,当 INSTANCE 已经存在时,线程先判断不为null,然后直接返回,避免了进入 synchronized 同步代码块。
思考里面的null判断是否可以去掉? ——不可以,因为只对 只对 INSTANCE = new Lock2Singleton() 加了锁,,防止第二个if和new操作间有别的线程进来,引起问题1
两次校验,一次不能少!
总结
单例模式双重校验模式完整代码:
public class Lock2Singleton {private volatile static Lock2Singleton INSTANCE; // 加 volatileprivate Lock2Singleton() {}public static Lock2Singleton getSingleton() {if (INSTANCE == null) { // 双重校验:第一次校验synchronized(Lock2Singleton.class) { // 加 synchronizedif (INSTANCE == null) { // 双重校验:第二次校验INSTANCE = new Lock2Singleton();}}}return INSTANCE;}
}
为什么是双重校验 ?
第二次校验是为了解决问题①,即避免多个线程重复创建对象。
第一次校验是为了提高效率,避免 INSTANCE 不为null时仍然去竞争锁。
为什么加 volatile ?
加 volatile 是为了禁止指令重排序,也就是为了解决问题②,即避免某个线程获取到其他线程没有初始化完全的对象。