单例模式(Singleton Pattern)
动机
在涉及同步问题的日志模块、缓存模块、多线程或线程池设计过程中,对于系统中的某些类来说,只有一个实例很重要。
如何保证一个类只有一个实例?定义一个全局变量并声明一个public方法以提供访问该全局变量的接口,可以确保该全局变量可被任何对象访问到,但缺不一定能保证该全局变量只被实例化一次。
一个更好的解决方案是让这个类自己负责该全局变量的实例化工作,并提供一个访问该全局变量的接口。这就是单例模式的动机。
定义
单例模式(Singleton Pattern)确保某一个类只有一个实例,且由这个类本身管理实例实例化的过程,并提供一个接口以允许整个系统访问这个实例,这个类称为单例类,
特点
- 某个类只有一个实例
- 类自身管理实例实例化的过程
- 提供接口以允许整个系统访问这个实例
构成
- 单例模式只包含一个角色,即单例类自身
- 单例类包含一个私有(private)的构造函数,以保证用户无法通过new关键字直接实例化它
- 单例类还包含一个静态(static)私有成员变量,和一个静态的公有(public)方法
- 该静态公有方法提供对单例对象的全局访问,并负责检查实例的存在性(若不存在则实例化单例对象,并保存在私有成员变量中),以保证整个系统中只有一个实例被创建了
示例
错误的实现[Bad implementation] - 懒汉式,线程不安全
当被问到要实现一个单例模式时,很多人的第一反应是写出如下的代码,包括教科书上也是这样教我们的。
public class Singleton {
private static Singleton instance;
private Singleton (){}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
这段代码简单明了,而且使用了懒加载模式,但是却存在致命的问题。
在多线程情况下,如果多个线程同时调用getInstance()的话,那么,可能会有多个进程同时通过 (singleton== null)的条件检查,于是,多个实例(通过new Singleton())就创建出来,并且很可能造成内存泄露问题。嗯,熟悉多线程的你一定会说——“我们需要线程互斥或同步”。
待优化的实现[Bad implementation] - 懒汉式,线程安全
为了解决上面的问题,最简单的方法是将整个 getInstance() 方法设为同步(synchronized)。
public class Singleton {
private static Singleton instance;
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
虽然做到了线程安全,并且解决了多实例的问题,但是它并不高效(虽然它的实现是完成正确的)。因为在任何时候只能有一个线程调用 getInstance() 方法。但是同步操作只需要在第一次调用时才被需要,即第一次创建单例实例对象时。这就引出了双重检验锁。
正确的实现
双重检验锁(double-check locking)
双重检验锁模式(double checked locking pattern),是一种使用同步块加锁的方法。程序员称其为双重检查锁,因为会有两次检查 instance == null
,一次是在同步块外,一次是在同步块内。为什么在同步块内还要再检验一次?因为可能会有多个线程一起进入同步块外的 if,如果在同步块内不进行二次检验的话就会生成多个实例了。
public class Singleton {
private static Singleton instance;
private Singleton()
public static Singleton getSingleton() {
if (instance == null) { //Single Checked
synchronized (Singleton.class) {
if (instance == null) { //Double Checked
instance = new Singleton();
}
}
}
return instance;
}
}
这段代码看起来很完美,很可惜,它是有问题。主要在于instance = new Singleton()
这句,这并非是一个原子操作,事实上在 JVM 中这句话大概做了下面 3 件事情:
- 给 Singleton对象实体在堆(heap)中分配内存
- 调用 Singleton 的构造函数来初始化Singleton对象实体的成员变量
- 将instance对象指向分配的内存空间(执行完这步 instance 就为非 null 了)
但是在 JVM 的即时编译器中存在指令重排序的优化。也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是后者,则在 3 执行完毕、2 未执行之前,被线程二抢占了,这时 instance 已经是非 null 了(但却没有初始化),所以线程二会直接返回 instance,然后使用,然后顺理成章地报错。
我们只需要将 instance 变量声明成 volatile 就可以了。
public class Singleton {
private volatile static Singleton instance; //声明成 volatile
private Singleton (){}
public static Singleton getSingleton() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
使用 volatile 有两个功用:
1)这个变量不会在多个线程中存在复本,直接从内存读取。
2)这个关键字会禁止指令重排序优化。也就是说,在 volatile 变量的赋值操作后面会有一个内存屏障(生成的汇编代码上),读操作不会被重排序到内存屏障之前。
下面一个程序用来尝试证明我的想法:
public class SingletonTest {
/**
* 使用volatile防止指令重排序的问题(singletonTest = new SingletonTest();)
* 不加volatile关键字也没有测试出来问题!!!!!
*/
public static SingletonTest singletonTest;
private int a;
private SingletonTest() {
a = 1;
}
/**
* synchronized双重锁机制
*/
public static SingletonTest getInstance() {
if (singletonTest == null) {
synchronized (SingletonTest.class) {
if (singletonTest == null) {
/*
* new操作非原子操作,分为3步:
* 1.为singletonTest分配分配内存
* 2.调用SingletonTest的构造函数初始化成员变量
* 3.将singletonTest指向分配的内存空间(执行完此步骤singletonTest对象就非null了)
* 若不加volatile关键字,2、3两步执行顺序不确定。按照1-3-2的顺序执行则可能会return未初始化的对象
* 时间 线程1 线程2
* t1 1
* t2 3
* t3 外层if (singletonTest == null)成立,直接return 未初始化的对象;
* t4 2
*/
singletonTest = new SingletonTest();
}
}
}
return singletonTest;
}
/**
* 测试函数
*/
public int getA() {
return a;
}
/**
* 启动1000个线程测试
*/
public static void main(String[] args) throws InterruptedException {
for (int j = 0; j < 10; j++) {
Thread[] threads = new Thread[10000];
CountDownLatch countDown = new CountDownLatch(10000);
for (int i = 0; i < threads.length; i++) {
threads[i] = new Thread(new Runnable() {
@Override
public void run() {
countDown.countDown();
SingletonTest instance = SingletonTest.getInstance();
if (instance.getA() != 1) {
System.err.println("error");
}
}
});
}
for (int i = 0; i < threads.length; i++) {
threads[i].start();
}
Thread.sleep(1000);
SingletonTest.singletonTest = null;
SingletonTest s = SingletonTest.singletonTest;
}
}
}
但是特别注意,在 Java 5 以前Java的版本中,如果使用 volatile 的双检锁还是会出现问题的。其原因是 Java 5 以前的 JMM (Java 内存模型)是存在缺陷的,即时将变量声明成 volatile 也不能完全避免重排序,主要是 volatile 变量前后的代码仍然存在重排序问题。这个 volatile 屏蔽重排序的问题在 Java 5 中才得以修复,所以在这之后才可以放心使用 volatile。
存疑
个人认为,其实不需要加 volatile 关键字,也是能够保证这个单例模式的正确性的。
原因在于,synchronized不仅能够保证原子性,同时保证可见性和有序性。即在结束synchronized代码块时,会把在synchronized代码块中进行的所有修改操作同步到主存中。
换句话说,只要在synchronized代码块中进行修改的变量,都隐式地声明为了volatile变量。
饿汉式单例类 static final field
这种方法非常简单,因为单例的实例被声明成 static 和 final 变量了,在Singleton类被第一次加载到内存中时,instance实例就会被初始化,所以创建实例本身是线程安全的。
public class Singleton{
//类加载时就初始化
private static final Singleton instance = new Singleton();
private Singleton(){}
public static Singleton getInstance(){
return instance;
}
}
private static final Singleton instance的声明和private的Singleton构造函数保证了当Singleton类被加载时,instance引用对象被赋值(而且只有这一次赋值机会)。
这种写法比较完美的话,唯一的缺点是它不是一种懒加载模式(lazy initialization),单例会在加载类后一开始就被初始化,即使客户端没有调用 getInstance()方法。饿汉式的创建方式在一些场景中将无法使用:譬如 Singleton 实例的创建是依赖参数或者配置文件的,在 getInstance() 之前必须调用某个方法设置参数给它,那样这种单例写法就无法使用了。
静态内部类 static nested class
我比较倾向于使用静态内部类的方法,这种方法也是《Effective Java》第一版中所推荐的。
public class Singleton {
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton (){}
public static final Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
这种写法仍然通过JVM本身的类加载机制来保证线程安全。
分析1
- 由于 SingletonHolder 是私有的,除了 getInstance() 之外没有办法访问到它,因此它是懒加载的(lazy initialization);同时读取实例的时候不会进行同步,没有性能缺陷;也不依赖 JDK 版本。
- 从外部无法访问静态内部类SingletonHolder,只有当调用Singleton.getInstance方法的时候,才能得到单例对象INSTANCE。
- INSTANCE对象初始化的时机,并不是在单例类Singleton被加载的时候,而是在第一次调用Singleton.getInstances()方法的时候,这个调用使得静态内部类SingletonHolder被加载,且此时INSTANCE引用对象被幅值。因此,这种实现方式是利用Classloader的加载机制来实现懒加载,并保证构建单例的线程安全。
分析2
不知道你有没有注意到,上面的三种实现方法都包含了一个private的构造函数。因此,我们是不是就能保证无法创建多个类的实例了呢?
答案是否定的,即我们仍然有其他的高阶方法来创建多个类的实施,以破解单例模式。
- 序列化(serialization)/反序列化(deserializations)
- 反射(reflection)
序列化(serialization)/反序列化(deserializations)问题
序列化可能会破坏单例模式。
通过比较一个序列化后的对象实例和其被反序列化后的对象实例,我们发现他们不是同一个对象,换句话说,在反序列化时会创建一个新的实例(即使定义构造函数为private)。
public class Example {
public static void main(String[] args) {
Singleton singleton = Singleton.getInstance();
singleton.setValue(1); // Serialize
try {
FileOutputStream fileOut = new FileOutputStream("out.ser");
ObjectOutputStream out = new ObjectOutputStream(fileOut);
out.writeObject(singleton);
out.close();
fileOut.close();
} catch (IOException e) {
e.printStackTrace();
}
singleton.setValue(2); // Deserialize
Singleton singleton2 = null;
try {
FileInputStream fileIn = new FileInputStream("out.ser");
ObjectInputStream in = new ObjectInputStream(fileIn);
singleton2 = (Singleton) in.readObject();
in.close();
fileIn.close();
} catch (IOException i) {
i.printStackTrace();
} catch (ClassNotFoundException c) {
System.out.println("singletons.SingletonEnum class not found");
c.printStackTrace();
}
if (singleton == singleton2) {
System.out.println("Two objects are same");
} else {
System.out.println("Two objects are not same");
}
System.out.println(singleton.getValue());
System.out.println(singleton2.getValue());
}
}
// 我们这里只是以 饿汉式 static final field的实现为例,但上面三种实现都有类似的问题
class Singleton implements java.io.Serializable {
//类加载时就初始化
private static final Singleton instance = new Singleton();
private int value;
private Singleton() {
}
public static Singleton getInstance() {
return instance;
}
public int getValue() {
return value;
}
public void setValue(int i) {
value = i;
}
}
结果:
Two objects are not same
2
1
反射(reflection)
类似地,使用反射(reflection)可以强行调用私有构造器。
public static void main(String[] args) throws Exception {
Singleton singleton = Singleton.INSTANCE;
Constructor constructor = singleton.getClass().getDeclaredConstructor(new Class[0]);
constructor.setAccessible(true);
Singleton singleton2 = (Singleton) constructor.newInstance();
if (singleton == singleton2) {
System.out.println("Two objects are same");
} else {
System.out.println("Two objects are not same");
}
singleton.setValue(1);
singleton2.setValue(2);
System.out.println(singleton.getValue());
System.out.println(singleton2.getValue());
}
结果:
Two objects are not same
1
2
解决
对于序列化时破坏单例模式的问题,我们虽然可以通过以下方式来避免:
//测试例子(四种写解决方式雷同)
public class Singleton implements java.io.Serializable {
public static Singleton INSTANCE = new Singleton();
protected Singleton() {
}
//反序列时直接返回当前INSTANCE
private Object readResolve() {
return INSTANCE;
}
}
因为,在反序列化过程中,如果被序列化的类中定义了readResolve 方法,虚拟机会试图调用对象类里的 readResolve 方法,以进行用户自定义的反序列化。
最终,实现了在序列化/反序列化过程中也不破坏单例模式。
类似地,对于反射时破坏单例模式的问题,我们虽然也可以通过以下方式来避免:
public static Singleton INSTANCE = new Singleton();
private static volatile boolean flag = true;
private Singleton(){
if(flag){
flag = false;
}else{
throw new RuntimeException("The instance already exists !");
}
}
但是,问题确实也得到了解决,但这样的解决方案使得代码并不优美。
那有没有更简单更高效的呢?当然是有的,那就是枚举单例了。
枚举 Enum
用枚举写单例实在太简单了!这也是它最大的优点。下面这段代码就是声明枚举实例的通常做法。
public enum SingletonEnum {
INSTANCE;
int value;
public int getValue() {
return value;
}
public void setValue(int value) {
this.value = value;
}
}
调用
public class EnumDemo {
public static void main(String[] args) {
SingletonEnum singleton = SingletonEnum.INSTANCE;
System.out.println(singleton.getValue());
singleton.setValue(2);
System.out.println(singleton.getValue());
}
}
我们可以通过EasySingleton.INSTANCE来访问实例,这比调用getInstance()方法简单多了。
由于默认枚举实例的创建是线程安全的,所以不需要担心线程安全的问题。但是在枚举中的其他任何方法的线程安全由程序员自己负责。还有防止上面的通过反射机制调用私用构造器。
这个版本基本上消除了绝大多数的问题。代码也非常简单,实在无法不用。这也是新版的《Effective Java》中推荐的模式。
分析
事实上,一个 enum类型是一个特殊的class类型。一个enum声明(declaration)实际上会被编译成这样:
final class SingletonEnum extends Enum
{
...
public static final SingletonEnum INSTANCE;
static
{
INSTANCE = new SingletonEnum("INSTANCE", 0);
$VALUES = (new SingletonEnum[] {
INSTANCE
});
}
}
当你的代码第一次访问SingletonEnum.INSTANCE时,SingletonEnum会被JVM加载并且初始化。
总结
以实例实例化的时间作为区分,可分为加载时实例化(Early Instantiation)和懒实例化(Lazy Instantiation):
- 加载时实例化(Early Instantiation):在整个系统开始运行时,就进行对象的实例化
- 懒实例化(Lazy Instantiation):直到该对象被第一次访问时,才进行实例化
优缺点
优点
- 由于在系统内存中只存在一个对象,因此可以节约系统资源,对于一些需要频繁创建和销毁的对象,单例模式无疑可以提高系统的性能
缺点
- 单例类的职责过重,在一定程度上违背了“单一职责原则”(Single Responsibility Principle)。因为单例类既充当了工厂角色(提供一个静态的公共方法以允许任何对象访问这个实例,即工厂方法),同时又充当了产品角色(即包含一些属于非静态的业务方法),最终将产品的创建和产品的本身的功能融合到一起。
- 滥用单例将带来一些负面问题,如为了节省资源将数据库连接池对象设计为单例类,可能会导致共享连接池对象的程序过多而出现连接池溢出。
适用场景
在以下场景下可以考虑使用单例模式:
- 在整个系统中,只能存在一个实例对象,如资源消耗较大的对象、配置管理类等;
- 在整个系统中,要求一个类只有一个实例。
模式应用
打印池
在操作系统中,打印池(Print Spooler)是一个用于管理打印任务的应用程序,通过打印池用户可以删除、中止或者改变打印任务的优先级,在一个系统中只允许运行一个打印池对象,如果重复创建打印池则抛出异常。
因此可使用单例模式来实现打印池。
ID生成器
在一个系统中,我们可能会使用到一个ID生成器,比如订单系统。我们需要保证其生成的每一个ID都是唯一且不会重复的。
为此,我们可以把这个IdGenerator设计成一个单例模式。
ConfigManager
对于一个系统而言,通常我们可能会设置一个配置管理中心,以管理一些对该系统的全局设置。
为此,我们可以把这个ConfigManager设计成一个单例模式,以保证这些设置都是全局唯一的,而不会出现一个设置项,在一个系统中,有两个不同的值的情况。
Reference
- 《Effective Java》
- 《Java与模式》
- Stacktips – Adapter Design Pattern In Java - http://stacktips.com/tutorials/design-patterns/adapter-design-pattern-in-java
- Graphic Design Pattern - 单例模式 - http://design-patterns.readthedocs.io/zh_CN/latest/creational_patterns/singleton.html
- 深入浅出单实例SINGLETON设计模式 - https://coolshell.cn/articles/265.html
- javatpoint - Singleton design pattern in Java - https://www.javatpoint.com/singleton-design-pattern-in-java
- 如何正确地写出单例模式 - http://wuchong.me/blog/2014/08/28/how-to-correctly-write-singleton-pattern/#stq=&stp=1
- 单例模式中volatile、synchronized指令重排序问题的一点疑问? - https://www.zhihu.com/question/265419026
- 漫画:什么是单例模式? - https://juejin.im/post/5a61a6856fb9a01c9b6605b6
- Why Enum Singleton are better in Java - https://javarevisited.blogspot.com/2012/07/why-enum-singleton-are-better-in-java.html
- Java Singletons Using Enum - https://dzone.com/articles/java-singletons-using-enum
- Implementing Singleton with an Enum (in Java) - https://stackoverflow.com/questions/26285520/implementing-singleton-with-an-enum-in-javas
- 深入理解Java枚举类型(enum) - https://blog.csdn.net/javazejian/article/details/71333103