设计模式 - 单例模式

1. 在Java中实现单例模式有哪些方法

单例模式是指一个类在任何情况下只有一个实例,并且提供全局访问点来获取该实例。

要实现单例模式,需满足

  1. 私有化构造方法,防止被外部实例化造成多实例问题;
  2. 提供一个静态方法作为全局访问点来获取唯一的实例对象。

实现单例的方法:

延迟加载

通过延迟加载的方式进行实例化,并增加了同步锁机制,避免多线程下的线程安全问题。

public class Singleton {
    private static Singleton instance;

    // 私有化构造函数,防止外部实例化
    private Singleton() {
        // 初始化代码
    }

    // 提供一个公共的静态方法来获取实例
    public static synchronized Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

缺点:有性能问题,同步锁只在第一次实例化时有用,后续不需要。

双重检查锁

双检锁来缩小锁的范围以提升性能

public class Singleton {
    private static volatile Singleton instance;

    // 私有化构造函数,防止外部实例化
    private Singleton() {
        // 初始化代码
    }

    // 提供一个公共的静态方法来获取实例
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

饿汉式

在类加载时就触发了实例化,从而避免多线程同步问题。

public class Singleton {
    private static Singleton instance = new Singleton();

    // 私有化构造函数,防止外部实例化
    private Singleton() {
    }

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

静态代码块

通过静态代码实例化,也能在类加载时触发并且只执行一次。

public class Singleton {
    private static Singleton instance = null;
    
    static {
        // 静态代码块用于初始化单例实例
        instance = new Singleton();
    }

    // 私有化构造函数,防止外部实例化
    private Singleton() {
    }

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

以上两种都是饿汉式,缺点是会降低启动速度,造成资源浪费。

静态内部类

把 INSTANCE 写在一个静态内部类中,由于只有在调用静态内部类的方法、静态域或者构造方法的时候才会加载静态内部类,所以当Singleton 被加载的时候不会初始化 INSTANCE,从而实现了延迟加载。

不存在线程安全问题

public class Singleton {
    private Singleton() {}
    
    private static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }
    
    public static Singleton getInstance() {
        return SingletonHolder.INSTANCE; // 首次调用时触发类加载
    }
}

枚举类

既是线程安全的,又能防止反序列化导致的破坏单例的问题。

// 枚举实现单例(隐式延迟加载)
public enum Singleton {
    INSTANCE;
    
    public void doSomething() {
        // 业务逻辑
    }
}

2. 哪些情况下的单例对象可能会被破坏

多线程

单例对象作为共享资源可能被多个线程同时操作,从而导致创建了多个对象。

可能出现在懒汉式单例中。

解决方案

  • 使用双检锁
  • 使用静态内部类

指令重排

memory = allocate()ctorInstance(memory)instance = memory执行指令:1. 内存分配2. 初始化对象3. 赋值引用memory = allocate()ctorInstance(memory)instance = memory指令重排:1. 内存分配2. 赋值引用3. 初始化对象instance = new Singleton()

instance = new Singleton()

这条赋值语句会被转换为3条指令,指令重排后,instance 指向分配好的内存的指令被排在了前面。在T1线程初始化这段内存之前,T2线程虽然不在同步代码块中,但是在同步代码块之前进行判断,会发现 instance 不为空,此时线程T2获得 instance 对象,直接使用就可能发生错误。

解决方案:在成员变量前加上 volatile, 保证所有线程可见性。

private static volatile Singleton instance = null;

克隆

Java中所有的类都继承自Object,都实现了clone() 方法,如果是深克隆,每次都会创建新的实例。

解决方法:重写clone() 方法,将单例自身的引用作为返回值。

反序列化

解决方法:在反序列化的过程中,Java API 会调用 readResolve() 方法,可以重写 readResolve() 方法,将返回值设置为已经存在的单例对象。

反射

解决放案:

  • 使用枚举实现单例:枚举类的构造函数会被 JVM 严格限制,反射无法调用。
  • 在构造方法中检查对象是否已被创建
private Singleton() {
    if (INSTANCE != null) {
        throw new IllegalStateException("单例已存在,禁止反射创建!");
    }
    // 初始化逻辑
}

3. 在DLC单例写法中,为什么主要做两次检查

第一次检查是为了保证只有首次并发时才出现阻塞,以此提高性能;第二次检查时为了保证不会重复创建对象;加锁以保证线程安全。

4. 哪些场景不适合使用单例模式

适用的场景:如果某个共享资源使用频次非常高,而且不可替代性也很强,就可以被设计为单例。比如,Spring IoC容器,JDK的Runtime.

不适用场景:要经常被赋值传递的对象 VO, POJO 等。