参考文章: 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;
}
}
饿汉与懒汉模式区别:
用于将对象的定义和初始化放在内部类中完成,在获取对象时要通过静态内部类调用其单例对象
之所以要这样设计,是因为类的静态内部类是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; // 加 volatile
private Lock2Singleton() {}
public static Lock2Singleton getSingleton() {
if (INSTANCE == null) { // 双重校验:第一次校验
synchronized(Lock2Singleton.class) { // 加 synchronized
if (INSTANCE == null) { // 双重校验:第二次校验
INSTANCE = new Lock2Singleton();
}
}
}
return INSTANCE;
}
}
为什么是双重校验 ?
第二次校验是为了解决问题①,即避免多个线程重复创建对象。
第一次校验是为了提高效率,避免 INSTANCE 不为null时仍然去竞争锁。
为什么加 volatile ?
加 volatile 是为了禁止指令重排序,也就是为了解决问题②,即避免某个线程获取到其他线程没有初始化完全的对象。
这是一个多线程单例模式场景:
现在有个多终端服务器,
服务器的开机和关机是非常耗时的(消耗性能),要尽量避免。
很多个人(线程)需要使用这个服务器,
第一个人来了,发现没有服务器是启动的,所以它选择开机(s生成实例)
第二个人来了,它也发现没有启动的服务器(第一个null判断),准备开机,它发现第一个人开机,所以它等待(volatile,而不是直接去开机)第一个人开机(开机要且只要一个人,synchroinzed)
开机成功后,第二个人看到开机成功了(第一个null判断),不再开机,直接获得服务器终端
第三个人来了,看到有服务器开机成功了(第一个null判断),所以他不需要等待,直接获得服务器终端
之后的人跟第三个人一样,不再需要开机,直接获得终端