单例模式

保证一个类中只有一个实例:用来维护某些全局资源、控制某些共享资源的访问权限;

提供一个访问该实例的全局节点,允许从程序中的任何地方访问该实例,该实例不会被其他变量覆盖;

单例的生命周期贯穿整个应用的生命周期,应该及时关闭销毁资源,避免出现内存泄露问题;

实现

Java 中实现单例模式的步骤:

  1. 将单例类的构造方法设置为 私有,防止其他方能够实例化该类,从而破坏单例模式;

  2. 声明一个 public static 方法用于返回单例对象;

Kotlin实现单例模式只需要用 object 或者 data object

饿汉式

在类加载的时候就完成对单例对象的初始化操作

优点:

  • 线程安全(类加载就是线程安全的)

  • 执行效率高(调用时无需进行判断以及创建对象)

缺点:

  • 当应用没有使用到该单例时,该单例一直存在于内存中,会造成内存的浪费;

class Singleton {
    
    private Singleton() {}
    
    private final static Singleton instance = new Singleton();

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

普通懒汉式

只有在需要用到该单例时才创建单例并初始化

优点:

  • 不会造成内存浪费,需要才创建;

缺点:

  • 线程不安全(特指下面的代码)

因为判断和创建操作并不是原子操作,在多线程环境下,可能存在多个线程同时创建单例

  • 线程A和线程B同时看到主存中的 instance 为 null,那么就会同时创建一次单例,也就是说在不同线程中的工作内存中存在不同的实例(不符合单例模式了)

  • 同时线程将 instance 刷新会主存的时机不确定,就可能导致最终主存中的 instance 是两个线程中其中的一个实例;

class Singleton {

    private Singleton() {}

    private static Singleton instance;

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

加锁懒汉式

要保证每个线程只有一个线程能够初始化单例,就需要进行加锁;

下面两种方法都保证同一时间只有一个线程能够创建实例;

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

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

当实例创建完后,每次访问还要加锁就会造成性能下降,因此按照思路可以改成这样:

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

上面的代码还是存在问题,当多个线程进入判断语句后,只有一个线程能够进入同步块,而该线程创建完后释放锁,其他线程就会进入该同步块,继续创建实例;

因此我们还需要在同步块再加上一次 if 判断,这也就是 双重检查锁

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

以为这样就结束了?NoNoNo;

Jvm 存在指令重排,在多线程下面可能会出现这种情况:线程B可能拿到的是一个没有调用过构造方法初始化过的实例对象;

时间

线程A

线程B

t1

A1:分配对象内存空间

t2

A2:设置instance指向内存空间

t3

B1:判断instance是否为空

t4

B2:由于instance不为null,线程B将访问instance引用的对象

t5

A3:初始化对象

t6

A4:访问instance引用的对象

虽然这种情况很少出现,但是出现了就导致未知的出错

这种情况就是因为指令重排造成的,如何防止指令重排呢? volatile 关键字就能防止修饰的变量出现指令重排

因此最终的代码如下:

class Singleton {

    private Singleton() { }

    private static volatile Singleton instance;

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

内部类实现

通过内部类延迟加载的特性(本质上是饿汉式,主类加载时并不会加载内部类, 用到了才会加载),将单例的实例化推迟到要用的时候;

  • 因为类加载是线程安全的,所以 instance 的创建也是线程安全的;

class Singleton {
 
    private Singleton () {}
 
    private static class SingletonHolder {
        private static final Singleton instance = new Singleton();
    }

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

关于内部类延迟加载可以使用下面的例子:

class Singleton {
    private static class SingletonHolder {
        static {
            System.out.println("Instantiation");
        }
        private static final Singleton instance = new Singleton();
    }

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

public class Main {
    public static void main(String[] args) {
        System.out.println("before using singleton");
        Singleton.getInstance();
    }
}
// 运行结果为:
// before using singleton
// Instantiation

枚举式单例

枚举类可以很好的实现单例模式(本质上是饿汉式单例模式,在语言层面实现线程安全)

  • 枚举类保证多线程同时访问时,只有一个实例对象被创建,不会出现线程安全问题;

  • 不能通过反射破坏单例(反射会检查类是否为枚举类,如果是则会抛出异常)

  • 不能通过反序列化破坏单例(每个枚举对象都有唯一 name 属性,序列化只会将 name 属性序列化;在反序列化过程中会通过该 name 属性返回当前已有的枚举对象)

例子代码:

enum Singleton {
    INSTANCE;
    
    private int counter = 0;
    
    public int getAndIncrement() {
        final int value = counter;
        counter ++;
        return value;
    }
}

使用方法:Singleton.INSTANCE.getAndIncrement()

容器式单例

当单例较多且这些单例大致上可以归为一类时,可以使用一个容器将这些单例存起来

class SingletonContainer {

    private static final Map<String, Object> container = new HashMap<>();

    private SingletonContainer() { }

    public static Object getInstance(String packageName) {
        if (container.containsKey(packageName)) {
            synchronized (SingletonContainer.class) {
                try {
                    Class<?> clazz = Class.forName(packageName);
                    Constructor<?> constructor = clazz.getDeclaredConstructor();
                    constructor.setAccessible(true);
                    Object object = constructor.newInstance();
                    container.put(packageName, object);
                    return object;
                } catch (ClassNotFoundException | InvocationTargetException | NoSuchMethodException |
                         InstantiationException | IllegalAccessException e) {
                    return null;
                }
            }
        }
        return container.get(packageName);
    }
}

破坏单例的方式

反射破坏单例

通过反射能够将重新创建一个新的单例;

  • 通过获取到单例类的构造方法,然后通过该构造方法获取即可;

try {
    Class<?> clazz = Singleton.class;
    Constructor<?> constructor = clazz.getDeclaredConstructor();
    constructor.setAccessible(true);
    Object object = constructor.newInstance();
    System.out.println(object);
    System.out.println(object == Singleton.getInstance());
} catch (Exception e) {
    //
}

对于饿汉式,可以在构造方法中加个判断:

class Singleton {
    
    private Singleton() {
		if (instance != null) {
			throw new RuntimeException("use `getInstance` instead.`");
		}
	}
    
    private final static Singleton instance = new Singleton();

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

对于懒汉式,则无法使用上面这种方法避免;

序列化破坏单例

如果某些单例类支持 序列化,那么可以先序列化再反序列化,反序列化会重新创建新的对象;

  • 如果构造方法中存在判断(如上面饿汉式的做法),反序列化会失败(因为反序列化也是调用反射执行)

try {
    FileOutputStream outputStream = new FileOutputStream("Singleton.obj");
    ObjectOutputStream out = new ObjectOutputStream(outputStream);
    // 对象序列化到文件中
    out.writeObject(SingletonDemo.getInstance());
    out.flush();
    out.close();
    outputStream.close();

    FileInputStream inputStream = new FileInputStream("Singleton.obj");
    ObjectInputStream inp = new ObjectInputStream(inputStream);
    // 文件中反序列化
    SingletonDemo instance = (SingletonDemo) inp.readObject();
    System.out.println(SingletonDemo.getInstance());
    System.out.println(instance);
    // 结果为: false
    System.out.println(instance == SingletonDemo.getInstance());
} catch (IOException | ClassNotFoundException e) {
    throw new RuntimeException(e);
}

对于实现了 Serializable 接口的解决方法:加上 readResolve() 方法,用此方法返回一个实例即可:

 @Serial
 private Object readResolve() {
     return instance;
 }

这时候反序列化就是同一个对象了

因为底层会通过反射判断是不是会有 readResolve() 这个方法,如果有则直接反射调用方法;