单例模式 Singleton
单例模式(Singleton):保证一个类仅有一个实例,并提供一个访问它的全局访问点。
A singleton is a class that ensures that only one instance of itself is created and provides a global point of access to that instance.
顾名思义,单例即单一的实例,确切地讲就是指在某个系统中只能存在一个实例,如果一个类只允许创建一个对象(或者实例),那这个类就是一个单例类,这种设计模式就叫单例模式。
一般来说,为了保证某个类只能生成一个对象,需要做三件事:
⨳ 构造器私有化,防止用户使用 new
创建对象
⨳ 封装单例类的唯一实例(如私有成员变量 instance
)
⨳ 向外暴露一个静态的公共方法(如 getInstance
),来获取对象
java
public class Singleton {
// 封装单例类的唯一实例
private static Singleton instance;
// 私有化构造方法
private Singleton(){}
public static Singleton getInstance(){
// 获取单一实例
return instance;
}
}
因为单例模式类封装了它的唯一实例,这样它可以严格地控制客户怎样访问它以及何时访问它。
基本实现
实现单例模式的方式有很多,现在就分别介绍一下:
饿汉式 Hungry
饿汉式是相对于懒汉式说的:
⨳ 饿汉式是在类加载的时候,instance
实例就已经创建并初始化好了;
⨳ 懒汉式并不是在类加载的时候实例化 instance
,而是调用 getInstance
方法时才进行实例化,即延迟加载(Lazy Loading)。
java
// 饿汉式
public class HungrySingleton {
private final static HungrySingleton instance;
static{
instance = new HungrySingleton();
}
private HungrySingleton(){}
public static HungrySingleton getInstance(){
return instance;
}
}
因为在类加载的时候,instance
静态实例就已经创建并初始化好了,所以,instance
实例的创建过程是线程安全的。
饿汉式的优点是提前实例化对象,没有线程安全问题,按照 fail-fast
的设计原则,有问题也可以提前暴露;而且如果单例对象初始化耗时长,饿汉式可以将耗时的初始化操作,提前到程序启动的时候完成,这样就能避免在程序运行的时候,再去初始化导致的性能问题。
缺点就是,如果这个单例一直没人用,提前初始化实例是一种浪费资源的行为,不如懒汉式的延迟加载。
懒汉式 Lazy
懒汉式并不是在类加载的时候实例化 instance
,而是调用 getInstance
方法时才进行实例化,即延迟加载(Lazy Loading)。
java
public class LazySingleton {
private static LazySingleton instance = null;
private LazySingleton(){}
public synchronized static LazySingleton getInstance(){
if(instance == null){
instance = new LazySingleton();
}
return instance;
}
}
注意,懒汉式的单例对象是在 getInstance
方法内部创建的,这会导致线程安全问题,所以 getInstance
方法要使用 synchronized
关键字修饰。
如果
getInstance
没有用synchronized
修饰,如果在多线程下,一个线程进入了if(instance == null)
判断语句块,还未来得及往下执行,另一个线程也通过了这个判断语句,这时便会产生多个实例。
给整个 getInstance
方法加锁,可以保证同一时刻只有一个线程进入方法内部,可以保证实例最多只创建一次,但也会导致每个线程想要通过 getInstance
方法实例时候,都要进行同步(串行操作),并发度很低。
其实 getInstance
方法只执行一次实例化代码就够了,后面的想获得该类实例,直接 return
就行了。
如果只在必要的地方加锁,即创建 instance
的时候加锁,instance
创建完成后,获取 instance
的时候不加锁,会大大提高并发性能。
这就是所谓的双重检测(Double Check)机制。
java
public class LazyDoubleCheckSingleton {
private volatile 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
:
⨳ 第一层 if
,如果 instance
已经创建初始化完成,则直接 return
,并没有锁;如果 instance
没有创建完,则进入同步代码块。
⨳ 第二层 if
,在同步代码块中,只会在 instance
为 null
的时候才会进来。
假如此时 instance
为 null
,并且同时有两个线程调用 getInstance
方法,它们将都可以通过第一层 if
的判断,然后由于锁机制,这两个线程则只有一个进入同步代码块,另一个在外排队等候,必须要其中的一个进入并出来后,另一个才能进入。
而此时如果没有了第二层的 if
的判断,则第一个线程创建了实例,而第二个线程还是可以继续再创建新的实例,这就是为什么需要两层判断。
还有一点, instance
成员变量被 volatile
关键字修饰了,volatile
的作用有两个:保证内存可见性 和 禁止指令重排序。
⨳ 保证内存可见性 :Java 内存模型规定,对一个 volatile
变量的写操作, Happens-Before 于后续对这个 volatile
变量的读操作(通过禁用 CPU 缓存实现)。
事实上,
Java
内存模型还规定,对一个锁的解锁 Happens-Before 于后续对这个锁的加锁,synchronized
关键字已经保证了对instance
变量操作的可见性,所以这里加volatile
关键字关键字最主要的意义是禁止指令重排序。
⨳ 禁止指令重排序:指令重排序是指编译器或 CPU 为了优化程序的执行性能,而对指令进行重新排序的一种手段。
比如说通过 new Singleton()
创建对象,我们以为的步骤是 ① 分配内存给这个对象;② 初始化对象;③ 设置 instance
变量指向刚分配的内存地址。
如果发生指令重排,这个顺序可能是 ① ③ ②,会在对象初始化之前就将刚分配的内存地址赋值给 instance
变量。如果线程A在执行完第 ① ③ 步后,执行步骤 ② 前,线程B又调用 getInstance
方法,发现 instance
变量已经不为 null
,从而线程B获取到的对象并不是初始化完的对象,导致线程B使用资格对象时,程序执行出错。
静态内部类 Static Inner Class
静态内部类是将单例对象的创建放到了静态内部类中,从而实现懒加载。
java
public class StaticInnerClassSingleton {
private StaticInnerClassSingleton(){}
// 静态内部类
private static class InnerClass{
private static StaticInnerClassSingleton instance = new StaticInnerClassSingleton();
}
public static StaticInnerClassSingleton getInstance(){
return InnerClass.instance;
}
}
InnerClass
是一个静态内部类,当外部类 StaticInnerClassSingleton
被加载的时候,并不会创建 InnerClass
实例对象。只有当调用 getInstance
方法时,InnerClass
才会被加载,从而完成 StaticInnerClassSingleton
的实例化。
类的静态属性只会在第一次加载类的时候初始化,所以在这里,
JVM
帮助我们保证了线程的安全性,在类进行初始化时,别的线程是无法进入的。
静态内部类实现单例,有点像饿汉式与懒汉式的结合体:
⨳ 对于静态内部类来说,单实例会随着内部类加载而完成实例化,是饿汉式;
⨳ 对于外部类来说,内部类的加载只会在调用 getInstance
方法时才会发生,为懒汉式。
枚举 Enume
枚举是 Java
中一种特殊的类,它可以定义固定数量的枚举实例,例如: 性别、季节等等。格式如下:
java
public enum Gender {
MALE, FEMALE; // 男,女
}
⨳ 枚举类的声明不再是使用 class
关键字,而是 enum
关键字;
使用 JAD 反编译后,enum
类为 public final class Gender extends Enum
,也就是枚举类,本质上就是继承 Enum
类、被 final
修饰的 class
类。
翻看 java.lang.Enum
,可以看到它有两个被枚举类继承的属性:
java
protected Enum(String name, int ordinal) {
this.name = name;
this.ordinal = ordinal;
}
⨳ MALE, FEMALE;
为枚举实例(也就是 Gender
对象),一般写在第一行,用逗号分隔,分号结尾。
使用 JAD 反编译后,这些枚举实例和饿汉式一样,在静态代码块中创建:
java
public static final Gender MALE;
public static final Gender FEMALE;
static
{
MALE = new Gender("MALE", 0);
FEMALE = new Gender("FEMALE", 1);
$VALUES = (new Gender[] {
MALE, FEMALE
});
}
枚举实现单例如下,这种方式是《Effective Java》
作者 Josh Bloch
提倡的方式:
java
public enum EnumSingleton {
INSTANCE; // 单例对象
private String someFiled; // 属性
public void doSomething(){}// 方法
}
单例破坏
单例破坏(Singleton Breakage):通常指尝试绕过单例模式的约束,创建多个实例。
反射
通过反射可以访问和修改类的私有构造方法,从而使得即使使用了单例模式,也可以创建多个实例,破坏了单例的唯一性。
以 HungrySingleton
为例:
java
HungrySingleton singleton = HungrySingleton.getInstance();
HungrySingleton singleton1 = HungrySingleton.getInstance();
System.out.println(singleton==singleton1); // true
// 反射调用私有构造方法
Constructor constructor = HungrySingleton.class.getDeclaredConstructor();
constructor.setAccessible(true);
HungrySingleton singleton2 = (HungrySingleton) constructor.newInstance();
System.out.println(singleton==singleton2); // false
对于懒汉式和静态内部类都可以使用同样的方式破坏单例,怎么预防反射破坏单例呢?
给构造方法里加上防御代码即可:
java
private HungrySingleton(){
if(instance != null){
throw new RuntimeException("单例构造器禁止反射调用");
}
}
这样如果单例对象已经存在的情况下,再次使用反射调用构造器就会报错。
这里需要注意一点,对于懒汉式单例,如果在调用 getInstance
生成对象前就使用反射创建对象,会绕过防御。
序列化与反序列化
在序列化和反序列化过程中,单例对象的状态可能会丢失,也会导致在反序列化时创建新的实例,破坏了单例模式的限制。
⨳ 序列化就是把 Java 对象转为⼆进制流,⽅便存储和传输。
⨳ 反序列化就是把⼆进制流恢复成对象。
对于需要序列化的对象需要实现
Serializable
接口,这个接口只是⼀个标记,没有具体的作⽤,但是如果不实现这个接口,在有些序列化场景会报错。序列化还涉及一个
serialVersionUID
的东西:
private static final long serialVersionUID = 1 L ;
ID 的数字其实不重要,只要序列化时候对象的
serialVersionUID
和反序列化时候对象的serialVersionUID
⼀致的话就⾏,如果没有显⽰指定serialVersionUID
,则编译器会根据类的相关信息⾃动⽣成⼀个。
以 HungrySingleton
为例,让其实现 Serializable
接口,就可以对其进行序列化操作了:
java
HungrySingleton singleton = HungrySingleton.getInstance();
HungrySingleton singleton1 = HungrySingleton.getInstance();
System.out.println(singleton==singleton1); // true
// 序列化
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton_file"));
oos.writeObject(singleton);
// 反序列化
File file = new File("singleton_file");
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
HungrySingleton singleton2 = (HungrySingleton) ois.readObject();
System.out.println(singleton==singleton2); // false
在分布式系统中,对象需要在网络中进行传输,而网络传输的数据必须是序列化的,以便能够在不同的节点之间传递。所以会频繁地涉及到序列化和反序列化传输对象的情况。
如果传输的对象是单例,那传输一次生成一个新的对象?不就破坏单例了嘛?
解决办法很简单,只要在单例中添加 readResolve()
方法即可:
java
private Object readResolve(){
return instance;
}
这是因为 ObjectInputStream
的 readObject
方法会检查类中是否有 readResolve()
方法。如果有的话,反序列化过程会调用该方法来获取实际返回的对象,而不是通过反序列化构造的新对象。
如果是懒汉式或静态内部类单例就这样加 readResolve
:
java
// 防止序列化破坏单例
protected Object readResolve() {
return getInstance();
}
再看枚举单例
在Java中,枚举类型的实例是在类加载时被创建的,并且在整个应用程序生命周期中只会有一个实例存在。当枚举类型的实例被序列化时,实际上是将枚举常量的名称写入到序列化输出流中,而不是将对象的状态写入。
在反序列化过程中,Java虚拟机会根据枚举常量的名称来查找对应的实例,而不是重新创建一个新的实例。这样就保证了在反序列化时获得的对象是相同的枚举实例,不会破坏枚举的单例性质。
ini
// ObjectInputStream#readEnum 节选
private Enum<?> readEnum(boolean unshared) throws IOException {
// ...
Enum<?> result = null;
Class<?> cl = desc.forClass();
if (cl != null) {
try {
@SuppressWarnings("unchecked")
Enum<?> en = Enum.valueOf((Class)cl, name);
result = en;
}
// ...
}
// ...
return result;
}
对于反射破坏的话,如果反射来创建新的枚举实例会抛出异常:
java
// Constructor#newInstance(Object ... initargs) 方法节选
if ((clazz.getModifiers() & Modifier.ENUM) != 0)
throw new IllegalArgumentException("Cannot reflectively create enum objects");
由于枚举类的特性,无法通过反射或反序列化来破坏单例模式。因此,在使用枚举类实现单例时,不需要额外处理反射或反序列化的情况。
这也是为什么《Effective Java》
作者 Josh Bloch
提倡使用枚举实现单例的原因。
源码鉴赏
JDK 之 Runtime
JDK 中的 Runtime
类是一个经典的懒汉单例模式实现,它提供了与运行时环境相关的操作和信息。代码节选如下:
java
public class Runtime {
private static Runtime currentRuntime = new Runtime();
public static Runtime getRuntime() {
return currentRuntime;
}
private Runtime() {}
// ...
}
Spring 之 Singleton
Spring中的Bean默认情况下作用域是单例的,容器负责创建Bean的实例,并在需要时将其提供给其他组件。
前文工厂模式讲了用于生产 Bean
的 Bean工厂 BeanFactory
,对于单例 Bean
来说,Spring
会将其放到DefaultSingletonBeanRegistry
中的容器中,这样再次获取该单例就不用重复创建了:
java
// `DefaultSingletonBeanRegistry` 节选
/** Cache of singleton objects: bean name to bean instance. */
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
@Nullable
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
Object singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
synchronized (this.singletonObjects) {
singletonObject = this.earlySingletonObjects.get(beanName);
if (singletonObject == null && allowEarlyReference) {
ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
if (singletonFactory != null) {
singletonObject = singletonFactory.getObject();
this.earlySingletonObjects.put(beanName, singletonObject);
this.singletonFactories.remove(beanName);
}
}
}
}
return singletonObject;
}
所以说,单例模式的实现方式有很多,不要局限于让类自身负责保存它的唯一实例,让其他容器保存对象实例也可以,只要调用同样的方法获取到对象都是一个,我都愿意称之为单例模式。
总结
那什么样的情况下需要系统中只存在单一的实例呢?
比如,系统的配置信息类。除此之外,我们还可以使用单例解决资源访问冲突的问题。
⨳ 资源共享:从业务概念上,有些数据在系统中只应该保存一份,就比较适合设计为单例类,比如配置信息类等,使用单例模式可以确保所有的组件都使用同一个实例。
⨳ 节省资源:有些对象创建和销毁的成本较高,而且在系统中只需要一个实例。这样的情况下,使用单例模式可以节省系统资源。
单例模式的缺点也显而易见,单例模式隐藏了类与类之间的依赖关系,增加了代码的耦合性,而且如果过度使用单例模式,将所有对象都设计成单例可能会导致代码结构复杂化,增加维护成本。
总之,设计模式只是解决特定问题的解决方案,没有最好,只有更合适。