单例模式之双重校验锁

参考文章: 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;    // 加 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判断),所以他不需要等待,直接获得服务器终端

之后的人跟第三个人一样,不再需要开机,直接获得终端

你可能感兴趣的:(单例模式,java,开发语言)