单例模式
什么是单例模式?
单例模式是指在内存中只会创建且仅创建一次对象的设计模式。在程序中多次使用同一个对象且作用相同时,为了防止频繁地创建对象使得内存飙升,单例模式可以让程序仅在内存中创建一个对象,让所有需要调用的地方都共享这一单例对象。
总结:单例模式顾名思义就是单例类只能有一个实例,且该类需自行创建这个实例,并对其他的类提供调用这一实例的方法。
单例模式优点和缺点
主要优点:
- 单例模式提供了对唯一实例的受控访问。因为单例类封装了它的唯一实例,所以它可以严格控制客户怎样以及何时访问它。
- 由于在系统内存中只存在一个对象,因此可以节约系统资源,对于一些需要频繁创建和销毁的对象单例模式无疑可以提高系统的性能。
- 允许可变数目的实例。基于单例模式我们可以进行扩展,使用与单例控制相似的方法来获得指定个数的对象实例,既节省系统资源,又解决了单例单例对象共享过多有损性能的问题。
主要缺点:
- 由于单例模式中没有抽象层,因此单例类的扩展有很大的困难。
- 单例类的职责过重,在一定程度上违背了“单一职责原则”。因为单例类既充当了工厂角色,提供了工厂方法,同时又充当了产品角色,包含一些业务方法,将产品的创建和产品的本身的功能融合到一起。
- 现在很多面向对象语言(如Java、C#)的运行环境都提供了自动垃圾回收的技术,因此,如果实例化的共享对象长时间不被利用,系统会认为它是垃圾,会自动销毁并回收资源,下次利用时又将重新实例化,这将导致共享的单例对象状态的丢失。
单例模式主要适用场景
在以下情况下可以考虑使用单例模式:
- 系统只需要一个实例对象,如系统要求提供一个唯一的序列号生成器或资源管理器,或者需要考虑资源消耗太大而只允许创建一个对象。
- 客户调用类的单个实例只允许使用一个公共访问点,除了该公共访问点,不能通过其他途径访问该实例。
单例模式实现
常见的单例模式实现方式有五种:饿汉式、懒汉式、双重检测锁、静态内部类和枚举单例。
饿汉式
饿汉式在类加载时已经创建好该对象,在程序调用时直接返回该单例对象即可,即我们在编码时就已经指明了要马上创建这个对象,不需要等到被调用时再去创建。
优点和缺点:
优点:
- 在类加载的同时已经创建好一个静态对象,调用时反应速度快。
- 基于 classloader 机制避免了多线程的同步问题,线程安全。
- 没有加锁,调用效率高。
缺点:
- 饿汉式在类加载时就初始化,浪费内存,不能延迟加载。
public class EagerSingleton {
/**
* 私有实例,静态变量会在类加载的时候初始化,是线程安全的
*/
private static EagerSingleton eagerSingleton = new EagerSingleton();
/**
* 私有构造方法
*/
private EagerSingleton() {
}
/**
* 唯一公开获取实例的方法(静态工厂方法)
*
* @return
*/
public static EagerSingleton getEagerSingleton() {
return eagerSingleton;
}
}
懒汉模式(线程不安全)
懒汉式就是“比较懒”,就是在用到的时候才去检查有没有实例,如果有则直接返回,没有则新建。
优点和缺点:
优点:
- 起到了懒加载的效果,但是只能在单线程下使用。
缺点:
- 线程不安全,如果在多线程下,两个线程同时进入了if(lazySingleton == null)判断语句块,这时便会产生多个实例。所以
在多线程环境下不可以使用这种方式。
public class LazySingleton {
/**
* 私有实例,初始化的时候不加载(延迟加载/懒加载)
*/
private static LazySingleton lazySingleton;
/**
* 私有构造
*/
private LazySingleton() {}
/**
* 唯一公开获取实例的方法(线程不安全)
*
* @return
*/
public static LazySingleton getInstance() {
if(lazySingleton == null) { // 使用的时候加载
lazySingleton = new LazySingleton();
}
return lazySingleton;
}
}
改良版线程安全的懒汉模式:
public class LazySingleton {
/**
* 私有实例,初始化的时候不加载(延迟加载/懒加载)
*/
private static LazySingleton lazySingleton;
/**
* 私有构造
*/
private LazySingleton() {}
/**
* 唯一公开获取实例的方法(线程安全,调用效率低)
*
* @return
*/
public synchronized static LazySingleton getLazySingleton() {
if(lazySingleton == null) { // 使用的时候加载
lazySingleton = new LazySingleton();
}
return lazySingleton;
}
}
上面代码中,通过关键字synchronized声明公共的获取实例的方法 getLazySingleton(),可以确保线程安全,能做到延迟加载,但是效率不高。
双重检查锁
public class SingletonByDoubleCheckLock {
/**
* 私有实例,初始化的时候不加载(延迟加载/懒加载)
*/
private static SingletonByDoubleCheckLock singleton;
/**
* 私有构造
*/
private SingletonByDoubleCheckLock(){}
/**
* 唯一公开获取实例的方法
*
* @return
*/
public static SingletonByDoubleCheckLock getInstance() {
if (singleton == null) { // 线程A和线程B同时看到singleton = null,如果不为null,则直接返回singleton
synchronized(SingletonByDoubleCheckLock.class) { // 线程A或线程B获得该锁进行初始化
if (singleton == null) { // 其中一个线程进入该分支,另外一个线程则不会进入该分支
singleton = new SingletonByDoubleCheckLock();
}
}
}
return singleton;
}
}
上面这段代码已经看似完美,但是还存在一个问题:指令重排序。
指令重排序:计算机在执行程序时候,为了提高代码、指令的执行效率,编译器和处理器会对指令进行重新排序,一般分为编译器对于指令的重新排序、指令并行之间的优化、以及内存指令的优化。
关于指令重排序的详细内容,请移步观看我的文章——JMM(Java 内存模型)详解
解决方案:使用volatile防止指令重排序
创建一个对象,在JVM中会经过三步:
- 为singleton分配内存空间
- 初始化singleton对象
- 将singleton指向分配好的内存空间
在这三步中,第2、3步有可能会发生指令重排现象,创建对象的顺序变为1-3-2,会导致多个线程获取对象时,有可能线程A创建对象的过程中,执行了1、3步骤,线程B判断singleton已经不为空,获取到未初始化的singleton对象,就会报空指针异常。文字较为晦涩,可以看流程图:
改良后的代码:
public class SingletonByDoubleCheckLock {
/**
* 私有实例,初始化的时候不加载(延迟加载/懒加载),使用volatile关键字,禁止指令重排序
*/
private volatile static SingletonByDoubleCheckLock singleton;
/**
* 私有构造
*/
private SingletonByDoubleCheckLock(){}
/**
* 唯一公开获取实例的方法
*
* @return
*/
public static SingletonByDoubleCheckLock getInstance() {
if (singleton == null) { // 线程A和线程B同时看到singleton = null,如果不为null,则直接返回singleton
synchronized(SingletonByDoubleCheckLock.class) { // 线程A或线程B获得该锁进行初始化
if (singleton == null) { // 其中一个线程进入该分支,另外一个线程则不会进入该分支
singleton = new SingletonByDoubleCheckLock();
}
}
}
return singleton;
}
}
静态内部类
该模式利用了静态内部类延迟初始化的特性,来达到与双重校验锁方式一样的功能。
- 利用了classloader机制来保证初始化 instance 时只有一个线程,线程安全;
- 只有通过显式调用 getInstance 方法时,才会显式装载静态内部类,从而实例化instance,延迟加载。
public class SingletonByStaticInnerClass {
/**
* 私有静态内部类
*/
private static class InnerClass{
// 初始化实例
private final static SingletonByStaticInnerClass INSTANCE = new SingletonByStaticInnerClass();
}
/**
* 私有构造
*/
private SingletonByStaticInnerClass() {}
/**
* 唯一公开获取实例的方法(静态工厂方法)
*
* @return
*/
public static SingletonByStaticInnerClass getInstance() {
return InnerClass.INSTANCE;
}
}
枚举
优点:
- 代码简洁 。
- 线程安全。
我们可以简单地理解枚举实现单例的过程:在程序启动时,会调用Singleton的空参构造器,实例化好一个Singleton对象赋给INSTANCE,
之后再也不会实例化
- 枚举类还自动支持序列化机制,使用枚举可以防止调用者使用反射、序列化与反序列化机制强制生成多个单例对象,破坏单例模式。
1. 枚举类默认继承了 Enum 类,在利用反射调用 newInstance() 时,会判断该类是否是一个枚举类,如果是,则抛出异常。
2. 在读入Singleton对象时,每个枚举类型和枚举名字都是唯一的,所以在序列化时,仅仅只是对枚举的类型和变量名输出到文件中,
在读入文件反序列化成对象时,利用 Enum 类的 valueOf(String name) 方法根据变量的名字查找对应的枚举对象。
所以,在序列化和反序列化的过程中,只是写出和读入了枚举类型和名字,没有任何关于对象的操作。
分析总结
- 在开发中如果对内存要求非常高,那么使用懒汉式写法,可以在特定时候才创建该对象;
- 如果对内存要求不高使用饿汉式写法,因为简单不易出错,且没有任何并发安全和性能问题;
- 为了防止多线程环境下,因为指令重排序导致变量报空指针异常,需要在单例对象上添加volatile关键字防止指令重排序;
- 最优雅的实现方式是使用枚举,其代码精简,没有线程安全问题,且 Enum 类内部防止反射和反序列化时破坏单例。