Singleton模式
当我们想在程序中表示某个东西只会存在一个时,就会有“只能创建一个实例”的需求。
- 确保任何情况下都绝对只有一个实例
- 在程序上表现出“只存在一个实例”
确保只生成一个实例的模式被称为Singleton模式。
实现
要保证“只有一个实例”,可以在每次调用时去判断是否已经有这个单例,如果有则返回,如果没有则创建。这就要求我们要控制构造函数是private
,这是为了禁止从Singleton类外部调用构造函数,通过new
来生成实例。
Lazy initialization [线程不安全]
思路很简单,那么尝试实现一个Singleton类。
public class Singleton {
private static Singleton singleton;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null)
instance = new Singleton();
return instance;
}
}
非常基本的实现方式,在调用getInstance()
方法时先判断是否已经存在,没有则创建,有则直接返回,同时还满足lazy loading
,避免内存浪费。满足懒加载的实现方式也称为懒汉式
。
但是这个实现最大的问题在于它是线程不安全的。在多线程的环境,某个线程通过instance == null
判断后,但是还未执行instance = new Singleton();
这个赋值操作时,其他的线程依旧可以通过instance == null
判断,最终就导致创建了不止一个实例。
Lazy initialization [方法同步-线程安全]
从现在开始追加一个条件,要求线程安全,那么应该怎么写?
public class Singleton {
private static Singleton singleton;
private Singleton() {}
public static synchronized Singleton getInstance() {
if (instance == null)
instance = new Singleton();
return instance;
}
}
在最开始的实现方式之上加锁即可,即使用synchronized
,通过同步方法来保证线程安全,同时也满足lazy loading
。但是同步方法的方式降低了效率,每个线程想获得实例的时候执行getInstance()
方法都要进行同步,可能多数情况下都不需要同步,这就会影响程序的效率。
Double-checked locking [线程安全]
那么能不能在此基础上优化一些呢?
public class Singleton {
private volatile static Singleton singleton;
private Singleton() {}
public static Singleton getInstance() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null)
singleton = new Singleton();
}
}
return singleton;
}
}
现在的实现与同步方法相比,只对局部的代码块进行了加锁,只需要在初始化的时候同步即可。
在通过第一次singleton == null
后,对代码块加锁,进行第二次singleton == null
检查。第二次的检查是必要的,因为可能当某个线程处于通过最初的检查后,在执行赋值操作之前时,存在其他线程也通过了最初的检查。
这种实现方式称为双重校验锁(DCL, double-checked locking)。
有意思的事情是,这种实现方式对JDK的版本有要求,即 JDK1.5 起才可以,这是为什么?
或许你还有别的疑问,比如修饰符volatile
是做什么用的?
这就要说说getInstance()
方法了,它存在另一个问题,这个问题产生的原因在于指令重排序
。
如果一个操作不是原子的,就会存在被JVM重排序的可能,比如instance= new Singleton()
,此赋值操作可以抽象为如下的JVM指令:
memory = allocate(); // 1:分配对象的内存空间
ctorInstance(memory); // 2:初始化对象
instance = memory; // 3:设置instance指向刚分配的内存地址
其中操作2依赖于操作1,但是操作3不依赖于操作2,所以JVM是可以针对它们进行指令的优化重排序的,经过重排序后如下:
memory = allocate(); // 1:分配对象的内存空间
instance = memory; // 3:设置instance指向刚分配的内存地址 此时对象还没有被初始化
ctorInstance(memory); // 2:初始化对象
可以看到指令重排之后,instance指向分配好的内存放在了前面,而这段内存的初始化被排在了后面。
当某个线程执行这段赋值语句,在初始化分配对象之前就已经将其赋值给instance引用,恰好另一个线程进入方法判断instance引用不为null,然后就将其返回使用,导致出错。
volatile
在JDK1.5之后,volatile
语义得到了加强,可以使用 volatile 禁止指令重排序。
volatile的作用在于:
- 局部阻止重排序的发生(对volatile变量的操作指令不会被重排序)
- 保证变量的可见性(对volatile的操作都在Main Memory中,而Main Memory是被所有线程所共享的)
这就是instance以关键字volatile修饰的原因以及为什么DCL对JDK有版本要求。
在保证可见性方面,锁(包括显式锁、对象锁)以及对原子变量的读写都可以确保变量的可见性。但是实现方式略有不同,例如同步锁保证得到锁时从内存里重新读入数据刷新缓存,释放锁时将数据写回内存以保数据可见,而volatile变量直接都是读写内存。
在JDK 5之前的版本使用“双重检查锁”会发生非预期行为 The “Double-Checked Locking is Broken” Declaration
此外还有类似如下几种实现方式。
饿汉式
public class Singleton {
private static final Singleton singleton = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return singleton;
}
}
通过ClassLoader机制避免了多线程的同步问题,保证初始化 instance 时只有一个线程,但是instance
在类装载时就实例化,如果导致类装载的原因不是调用getInstance()
方法, 就会造成内存的浪费,显然不符合lazy loading
。
每个类编译后产生一个class对象,存储在.class文件中,JVM使用类加载器(Class Loader)来加载类的字节码文件(.class)。
所有的类都是在第一次被用到时,动态加载到JVM的,对象在class文件加载完毕,以及为各成员在方法区开辟好内存空间之后,就开始所谓“初始化”的步骤:
- 基类静态代码块,基类静态成员字段(并列优先级,按代码中出现先后顺序执行,只有第一次加载类时执行)
- 派生类静态代码块,派生类静态成员字段(并列优先级,按代码中出现先后顺序执行,只有第一次加载类时执行)
- 基类普通代码块,基类普通成员字段(并列优先级,按代码中出现先后顺序执行)
- 基类构造函数
- 派生类普通代码块,派生类普通成员字段(并列优先级,按代码中出现先后顺序执行)
- 派生类构造函数
注意,第1,2步的静态过程,只在这个类第一次被加载的时候才运行,正是如此保证了只有一个Singleton实例。
静态内部类形式
public class Singleton {
private static class SingletonHolder {
private static final Singleton singleton = new Singleton();
}
private Singleton() {}
public static Singleton getInstance() {
return SingletonHolder.singleton;
}
}
这种方式与饿汉式都是采用了类装载的机制来保证初始化实例时只有一个线程。
但是饿汉式是只要 Singleton 类被装载就会实例化,而静态内部类方式在 Singleton 类被装载时并不会立即实例化。
只有调用getInstance()
方法,才会装载 SingletonHolder 类,从而完成 Singleton 的实例化。
枚举形式
public enum Singleton {
singleton;
public void doSomething() {}
}
需要注意enum
是在 JDK 1.5 之后才引入的。Java规范中规定,每一个枚举类型及其定义的枚举变量在 JVM 中都是唯一的,在枚举类型的序列化和反序列化上,Java做了特殊的规定。
1.12 Serialization of Enum Constants
Enum constants are serialized differently than ordinary serializable or externalizable objects. The serialized form of an enum constant consists solely of its name; field values of the constant are not present in the form. To serialize an enum constant, ObjectOutputStream writes the value returned by the enum constant’s name method. To deserialize an enum constant, ObjectInputStream reads the constant name from the stream; the deserialized constant is then obtained by calling the java.lang.Enum.valueOf method, passing the constant’s enum type along with the received constant name as arguments. Like other serializable or externalizable objects, enum constants can function as the targets of back references appearing subsequently in the serialization stream.
The process by which enum constants are serialized cannot be customized: any class-specific writeObject, readObject, readObjectNoData, writeReplace, and readResolve methods defined by enum types are ignored during serialization and deserialization. Similarly, any serialPersistentFields or serialVersionUID field declarations are also ignored–all enum types have a fixed serialVersionUID of 0L. Documenting serializable fields and data for enum types is unnecessary, since there is no variation in the type of data sent.
登场角色
Singleton:在Singleton模式下,只有Singleton这一个角色。Singleton角色中有一个返回唯一实例的静态方法。该方法总是返回同一个实例。
要点思考
单例模式可以说是最简单的设计模式之一了,但是从各种实现方式的角度说,涉及到了许多知识点。
对于饿汉式与静态内部类形式的实现,要求我们理解类加载的机制,对于懒汉式或是DCL形式的实现,要求我们了解synchronized
、volatile
的使用,以及知道指令重排序的存在。
对于线程安全相关的知识要学习的还有很多,文中只是对相关概念有了初步的了解,今后还需深入学习。