Java 设计模式之单例模式

单例模式

定义:保证一个类有且仅有一个实例,并提供一个全局访问点

适用场景:想确保任何情况下都绝对只有一个实例

优点

  • 在内存里只有一个实例,减少了内存开销
  • 可以避免对资源的多重占用
  • 设置全局访问点,严格控制访问

缺点:没有接口,扩展困难

特点

  • 私有构造器(即被 private 修饰构造方法)
  • 线程安全
  • 延迟加载
  • 序列化和反序列化安全、
  • 反射

饿汉式单例

饿汉式单例是类进行初始化的时候,就已经把对象创建好了,并且使用 final 修饰,因为 final 关键字在类初始化时就必须把变量初始化好,并且不可改变,很符合单例模式的特征。

public class HungrySingleton {

    private final static HungrySingleton instance;

    static {
        instance = new HungrySingleton();
    }

    private HungrySingleton() {
    }

    public static HungrySingleton getInstance() {
        return instance;
    }
}

懒汉式单例

注重的是 延时加载 ,就意味着只有在使用它的时候,才开始初始化,不使用则不会初始化,
以下是线程不安全的懒汉式单例模式代码示例

/**
 * @author Hyxiao
 * @date 2022/3/15 17:02
 * @description 单例模式-懒汉模式(懒->初始化的时候没有创建对象)
 */
public class LazySingleton {

    private static LazySingleton instance = null;
    private LazySingleton() {}
    
    public static LazySingleton getInstance(){
        if (instance == null){
            instance = new LazySingleton();
        }
        return instance;
    }

}

线程安全的懒汉式单例模式代码示例

public class LazySingleton {

    private static LazySingleton instance = null;
    private LazySingleton() {}
    
    public synchronized static LazySingleton getInstance(){
        if (instance == null){
            instance = new LazySingleton();
        }
        return instance;
    }

}

需要留意的是,当用 synchronized 修饰静态 static 方法时,相当于锁的是 LazySingleton 的class文件,也就是把这个类给锁住了;而用 synchronized 修饰普通方法时,锁的是在堆内存中生成的对象。

等同于以下这种写法(锁住了整个类)

public class LazySingleton {

    private static LazySingleton instance = null;
    private LazySingleton() {}
    
    public static LazySingleton getInstance(){
        synchronized (LazySingleton.class){
            if (instance == null){
            	instance = new LazySingleton();
        	}
        }
        return instance;
    }

}

双重检查懒汉式单例

public class LazyDoubleCheckSingleton {

    private static LazyDoubleCheckSingleton instance = null;

    private LazyDoubleCheckSingleton() {}

    public static LazyDoubleCheckSingleton getInstance() {
        if (instance == null) { 
            synchronized (LazyDoubleCheckSingleton.class) {
                if (instance == null) {
                    instance = new LazyDoubleCheckSingleton();
                }
            }
        }
        return instance ;
    }

}

这种写法有隐患,具体体现在 if (instance == null) {instance = new LazyDoubleCheckSingleton();这两个地方

程序会先对进来的 instance 对象进行判空,会出现 instance 不为 null 的情况,这时候虽然 instance 是不为 null 的,但是 instance 还没有完成初始化的过程,就是 instance = new LazyDoubleCheckSingleton(); 没有执行完。针对这种 instance 会为 null 实例的场景,为此我们提出两种解决方案,一种是 保证类初始化过程的有序性 ,另一种是 类初始化时,隔离其他线程干扰

通常当我们创建并初始化一个 LazyDoubleCheckSingleton 对象时,正常情况下要经历以下三个步骤:

instance = new LazyDoubleCheckSingleton();
  1. 分配内存给这个对象
  2. 初始化对象
  3. 设置 instance 指向步骤 1 刚分配好的内存地址

但是按上面所说的特殊情况,程序可能会碰到当执行完步骤 1 后,步骤 2 和 3 很有可能会出现顺序颠倒,也就是重排序,也就是下面这种情况

  1. 分配内存给这个对象
  2. 设置 instance 指向步骤 1 刚分配好的内存地址
  3. 初始化对象

所以,当出现重排序情况时, 也就是 instance 已经指向分配好的内存地址,但是 instance 它是没有初始化完成的。也就是说在多线程并发的情况下,其他线程进来拿到 instance ,由于 instance 已经分配好了内存地址,所以 instance 不为 null ,就直接返回 instance 这个没有初始化的实例,系统就会报异常。

而对于单线程情况下,这种重排序的特殊情况,是不会有什么影响的,不会改变程序的执行结果,Java 语言规范是允许那些在单线程内不会改变单线程程序执行结果的重排序,因为单线程下的重排序,反而能提高执行性能。

保证类初始化过程的有序性

为了避免出现这种步骤 2 和 3 重排序的问题,我们可以通过 volatile 关键字来修饰 instance 来实现线程安全的延迟初始化,从而禁止重排序。如下示例代码,在声明 instance 实例时采用 volatile 来修饰,来保证步骤 2 和 3 的有序执行,防止出现重排序

private volatile static LazyDoubleCheckSingleton instance = null;

类初始化时,隔离其他线程干扰

除了使用 volatile 来限制重排序以外,我们还能通过静态内部类的方式;因为 JVM 在类的初始化阶段,会去获取一个锁,这个锁会同步多个线程对一个类的初始化。这样当其中一个线程在创建并初始化一个单例类的时候,其他线程是无法得知这个类的具体情况的,这也就保证了即使出现重排序,但是其他线程也无法获得到这个类的实例。

public class StaticInnerClassSingleton {

    private StaticInnerClassSingleton(){}
    
    private static class InnerClass {
        private static StaticInnerClassSingleton instance = new StaticInnerClassSingleton();
    }

    public static StaticInnerClassSingleton getInstance() {
        return InnerClass.innerClassSingleton;
    }

}
页面下部广告