单例模式(Singleton Pattern):确保一个类只有一个实例,并提供一个全局访问点。比如,线程池、缓存等。

如果不用单例模式,那有什么办法能创建一个独一无二的对象吗?

可以使用 Java 的静态变量。

使用静态变量有什么缺点吗?

如果要将对象赋值给一个全局变量,那么在程序一开始就要创建好这个对象。如果创建这个对象需要消耗比较大的资源,同时本次程序的执行过程中又没有用到它,这就会很浪费。

使用单例模式就可以做到,在需要时才创建对象(lazy instantiaze)。

下面是经典的单例模式代码:

public class Singleton_1 {

// 使用静态变量来记录唯一实例
private static Singleton_1 uniqueInstance;

// 构造器私有化,只有 SingletonPattern_1 类内才可以调用构造器
private Singleton_1() {}

// 提供一个全局访问点,返回 uniqueInstance
public static Singleton_1 getInstance() {
if (uniqueInstance == null) {
uniqueInstance = new Singleton_1();
}
return uniqueInstance;
}
}

经典单例模式实现有什么问题吗?

在多线程环境下,如果两个线程一前一后都判断 if (uniqueInstance == null) 成立,则两个线程就会创建两个不同对象。这就违反了单例模式的初衷。

有什么办法解决吗?

getInstance() 方法上加 synchronized 关键字,保证同一时刻,只有一个线程可以去执行 new Singleton_1() 方法,其他线程在外面等待。

这样给 getInstance() 方法上加 synchronized 关键字会不会降低程序性能呢?还有其他什么好办法吗?(当然,如果 getInstance() 的性能对应用程序不是很关键,则完全不用进行优化)

优化方法 1:使用「急切(eagerly)」创建实例

public class Singleton_2 {

// 在静态初始化器中创建单例实例,这样 JVM 可以保证在任何线程访问 uniqueInstance 静态变量之前,一定先创建此实例
private static Singleton_2 uniqueInstance = new Singleton_2();

private Singleton_2() {}

// 已经有 uniqueInstance 实例了,直接返回
public static Singleton_2 getInstance() {
return uniqueInstance;
}
}

优化方法 2:使用「双重检查加锁」,在 getInstance() 减少使用同步

public class Singleton_3 {

// volatile 关键字确保,当 uniqueInstance 变量被初始化成 Singleton_3 实例时,多个线程正确地处理 uniqueInstance 变量
private volatile static Singleton_3 uniqueInstance;

private Singleton_3() {}

// 使用双重检查加锁,首先检查是否实例已经创建,如果尚未创建,才进行同步。这样只有第一次会同步
public static Singleton_3 getInstance() {
if (uniqueInstance == null) {
synchronized (Singleton_3.class) {
if (uniqueInstance == null) {
uniqueInstance = new Singleton_3();
}
}
}
return uniqueInstance;
}
}

这样可以保证线程安全了,但是 Java 中还有没有什么能力可以创建对象呢?

除了通过 new 一个对象外,还可以使用克隆、反射和反序列化等技术创建一个对象。这就对上述的单例模式造成了破坏。

  • 克隆:使用浅拷贝的方式创建一个对象

  • 反射:反射可以绕过构造函数的访问控制(通过 setAccessible(true) 允许访问私有构造函数),从而直接创建类的实例

  • 反序列化:在反序列化时,Java 会通过 readObject() 方法重新创建对象

那怎么解决呢?使用枚举类。

public enum Singleton_4 {
UNIQUE_INSTANCE;
}

枚举类是在 JVM 的加载过程中自动处理的,枚举类的实例在类加载的过程中就已经被创建。并且 JVM 会保证枚举实例的唯一性。枚举类的构造方法是私有的。

克隆:枚举类无法克隆

反射:通过反射调用枚举类的构造方法会抛异常:java.lang.reflect.InvocationTargetException

反序列化:对于枚举类,JVM 会自动处理反序列化,并确保反序列化时返回的是枚举类的唯一实例