概念
确保某一个类只有一个实例,而且自行实例化,并向整个系统提供一个访问它的全局访问点,这个类称为单例类。
特性
单例类只能有一个实例
单例类必须自行创建自己的唯一的实例
单例类必须给所有其他对象提供这一实例
应用场景
- 系统只需要一个实例对象,如系统要求提供一个唯一的序列号生成器或资源管理器,或者需要考虑资源消耗太大而只允许创建一个对象
- 在整个项目中需要一个共享访问点或共享数据,除了该公共访问点,不能通过其他途径访问该实例
- 创建一个对象需要消耗的资源过多,如要访问IO和数据库等资源
- 需要定义大量的静态常量和静态方法(如工具类)的环境,可以采用单例模式(当然,也可以直接声明为static的方式)
- 资源共享的情况下,避免由于资源操作时导致的性能或损耗等。如上述中的日志文件,应用配置,数据库连接池。
- 控制资源的情况下,方便资源之间的互相通信。如线程池等。
优缺点
优点:
- 由于单例模式在内存中只有一个实例,减少了内存开支,特别是一个对象需要频繁地创建销毁时,而且创建或销毁时性能又无法优化,单例模式的优势就非常明显
- 由于单例模式只生成一个实例,所以减少了系统的性能开销,当一个对象的产生需要比较多的资源的时候,如读取配置,产生其他的依赖对象时,可以通过在应用启动的时候直接产生一个单例对象,然后用永久驻留内存的方式来解决
- 单例模式可以避免对资源的多重占用,例如对一个写文件动作,由于只有一个实例存在内存中,避免对同一个资源文件的同时写操作
- 单例模式可以在系统设置全局的访问点,优化和共享资源访问,例如可以设计一个单例类,负责所有数据表的映射处理
缺点:
- 单例模式一般没有接口/抽象层,扩展困难。
- 单例类的职责过重,在一定程度上违背了“单一职责原则”,一个类应该只实现一个逻辑,而不关心他是否是单例的,是不是要单例取决于环境。因为单例类既充当了工厂角色,提供了工厂方法,同时又充当了产品角色,包含一些业务方法,将产品的创建和产品的本身的功能融合到一起。
- 现在很多面向对象语言(如Java、C#)的运行环境都提供了自动垃圾回收的技术,因此,如果实例化的共享对象长时间不被利用,系统会认为它是垃圾,会自动销毁并回收资源,下次利用时又将重新实例化,这将导致共享的单例对象状态的丢失。
代码实现
- 懒汉式(线程安全、线程不安全)
- 饿汉式(线程安全)
- 双重检查加锁
- 静态内部类
- 枚举单例
1. 懒汉式、线程不安全
这种方式是最基本的实现方式,这种实现最大的问题就是不支持多线程。因为没有加锁synchronized,所以严格意义上它并不算单例模式。
与饿汉式的不同:不是一看到 instance 就初始化,饱汉要等到第一次使用的时候才初始化,不像饿汉一样一见到 instance 就初始化,这也被称为 懒加载,如果系统中很多这样的类,显然是懒加载的时候效率更高
public class Singleton {
// 4:定义一个变量来存储创建好的类实例(关键点:声明单例对象是静态的)
// 5:因为这个变量要在静态方法中使用,所以需要加上static修饰
private static Singleton instance;
// 1:私有化构造方法,好在内部控制创建实例的数目,限制产生多个对象(关键点:构造函数是私有的)
private Singleton() {}
// 2:定义一个方法来为客户端提供类实例
// 3:这个方法需要定义成类方法,也就是要加static
// 定义一个静态方法来为客户端提供类实例(全局访问点),这样就不需要先得到类实例
public static Singleton getInstance() {
// 6:判断存储实例的变量是否有值(关键点:判断单例对象是否已经被构造)
if(instance == null) {
// 6.1:如果没有,就创建一个类实例,并把值赋值给存储类实例的变量
instance = new Singleton();
}
// 6.2:如果有值,那就直接使用
return instance;
}
}
为什么这种实现是线程不安全的呢?如一个线程A执行到singleton = new Singleton();这里,但还没有获得对象(对象初始化是需要时间的),第二个线程B也在执行,执行到if(singleton == null)判断,那么线程B获得判断条件也是为真,于是继续运行下去,线程A获得了一个对象,线程B也获得了一个对象,在内存中就出现两个对象,造成单例模式的失效!!
2. 懒汉式、线程安全
第一次调用才初始化,避免内存浪费。绝对线程安全,但是效率很低,99%情况下不需要同步。必须加锁 synchronized 才能保证单例,但加锁会影响效率,并发性能极差,事实上完全退化到了串行。
public class Singleton {
private static Singleton singleton;
private Singleton() {}
// 加锁 synchronized
public static synchronized Singleton getInstance() {
if(instance == null) {
instance = new Singleton();
}
return instance;
}
}
或者也可以这样:
public class Singleton {
private static Singleton instance = null; // 关键点 1:声明单例对象是静态的
private Singleton() {} // 关键点 0:构造函数是私有的
private static Object obj = new Object();
public static Singleton GetInstance() { // 通过静态方法来构造对象
if (instance == null) { // 关键点 2:判断单例对象是否已经被构造
lock(obj) { // 关键点 3:加线程锁
instance = new Singleton();
}
}
return instance;
}
}
虽然这里判断了一次单例对象是否已经被构造,但是由于某些情况下,可能有延迟加载或者缓存的原因,只有关键点 2 这一次判断,仍然不能保证系统是否只创建了一个单例,也可能出现多个实例的情况。
3. 饿汉式
这种方式比较常用,但容易产生垃圾对象。
优点:没有加锁,执行效率会提高。
缺点:类加载时就初始化,浪费内存。
饿汉单例模式线程安全。
值得注意的时,单线程环境下,饿汉与饱汉在性能上没什么差别;但多线程环境下,由于饱汉需要加锁,饿汉的性能反而更优。
public class Singleton {
// 定义一个静态变量来存储创建好的类实例,直接在这里创建类实例,只会创建一次
private static Singleton instance = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return instance;
}
}
也可以这样写(使用静态初始化块):
public class Singleton {
private static Singleton instance = new Singleton();
static {
instance = new Singleton();
}
private Singleton() {}
public static Singleton getInstance() {
return instance;
}
}
4. 双重检查锁(DCL,即 double-checked locking)
没有volatile修饰instance的双重检查锁版本仍然是线程不安全的,由于指令重排序,你可能会得到 “半个对象”。
如果使用双重检查锁定来实现懒汉式单例类,需要在静态成员变量instance之前增加修饰符volatile,被volatile修饰的成员变量可以确保多个线程都能够正确处理。
由于volatile关键字会屏蔽Java虚拟机所做的一些代码优化,可能会导致系统运行效率降低,因此即使使用双重检查锁定来实现单例模式也不是一种完美的实现方式。
public class Singleton {
private static volatile Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
所以,在判断单例实例是否被构造时,需要检测两次,在线程锁之前判断一次,在线程锁之后判断一次,再去构造实例,这样就万无一失了。
或者也可以这样:
public class Singleton {
private static Singleton instance = null; // 关键点 1:声明单例对象是静态的
private Singleton() {} // 关键点 0:构造函数是私有的
private static Object obj = new Object();
public static Singleton getInstance() { // 通过静态方法来构造对象
if (instance == null) { // 关键点 2:判断单例对象是否已经被构造
lock(obj) { // 关键点 3:加线程锁
if (instance == null) { // 关键点 4:二次判断单例是否已经被构造
instance = new Singleton();
}
}
}
return instance;
}
}
这个版本看出优秀在哪里了吗?
- 懒加载
- 确保线程安全
- 只有第一次创建类的时候可能发生阻塞,后面由于非空判断都不会阻塞
- volatile 用来保证多个线程并发时,访问的都是内存中的同一个 volatile 对象。
缺点就是仍然可以通过反射等方式产生多个对象!
5. 静态内部类/Holder模式
我们既希望利用饿汉模式中静态变量的方便和线程安全;又希望通过懒加载规避资源浪费。Holder 模式满足了这两点要求:核心仍然是静态变量,足够方便和线程安全;通过静态的 Holder 类持有真正实例,间接实现了懒加载。
原理就是说,静态内部类会在第一次被使用的时候被初始化,并且也只会被初始化一次,所以也包含懒加载和线程安全的特性。
public class Singleton {
private Singleton() {}
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
}
StaticSingleton 被加载时,内部类不会被实例化,确保 StaticSingleton 类被载入 jvm 时,不会被初始化单例类,而当 getInstance() 方法被调用时,才加载 SingletonHolder,从而初始化 instance。同时用于实例的建立在类加载时完成,故天生对线程友好。
使用内部类完成单利模式,既可以做到延迟加载,也不用使用同步关键字,是一种比较完善的做法。
这种写法仍然使用 JVM 本身机制保证了线程安全问题;由于 SingletonHolder 是私有的,除了 getInstance() 之外没有办法访问它,因此它是懒汉式的;同时读取实例的时候不会进行同步,没有性能缺陷;也不依赖 JDK 版本。
6. 枚举单例
用枚举实现单例模式,相当好用,但可读性是不存在的。
枚举类型也是在第一次被使用的时候初始化,并且默认构造函数是 private 修饰,而且线程安全。
以上的单例还是在运用各种技巧来实现,最后一种简直是利用规则来实现。这种代码也及其简洁,只需要三行就可以实现,但是可惜的是在面试中并不适用,因为很多面试官可能也并不了解这个特性,那就是枚举类。Java 的枚举类是天生单例的,并且能够对多线程免疫,对序列化免疫,简直是神器。
// 将枚举的静态成员变量作为单例的实例
public enum Singleton {
INSTANCE;
}
面试问题
单例模式实现的关键点
- 私有构造函数(private):不能由其他类任意 new 单例模式的类
-
getInstance()
是静态方法(static):因为用了类名.getInstance()
来调用方法 - 声明静态单例对象:因为
getInstance()
方法是static的,由于静态方法只能访问静态变量,所以单例对象instance也必须是static的,而且静态变量只会被初始化一次 - 构造单例对象之前要加锁(lock 一个静态的 object 对象)
- 需要两次检测单例实例是否已经被构造,分别在锁之前和锁之后
为何要检测两次?
有可能延迟加载或者缓存原因,造成构造多个实例,违反了单例的初衷。
构造函数能否公有化?
不行,单例类的构造函数必须私有化,单例类不能被实例化,单例实例只能静态调用
lock 住的对象为什么要是 object 对象,可以是 int 吗?
不行,锁住的必须是个引用类型。如果锁值类型,每个不同的线程在声明的时候值类型变量的地址都不一样,那么上个线程锁住的东西下个线程进来会认为根本没锁,相当于每次都锁了不同的门,没有任何卵用。而引用类型的变量地址是相同的,每个线程进来判断锁多想是否被锁的时候都是判断同一个地址,相当于是锁在通一扇门,起到了锁的作用。
如何选择各种实现方式
俗话说,No silver bullet,每一种实现都有其适用的场景。那么,我们如何选择单例的实现方式呢?答案是:取决于你所期望的内容。
如果你的单例类应用频繁,从系统启动后就需要使用,那么,饿汉式可能是一个不错的选择。类加载过程便已经完成了实例化的单例,在之后的调用过程中,无需再进行实例化,也无需害怕因为线程同步导致的性能损耗。
如果你的单例类占用较多资源,并且调用频率较低,那么或许 Double-Check 的懒汉式是一个不错的选择。在单例使用前,并不会被实例化,其所需要的资源也并不会被占用。
如果你的单例类属于某一个类库,或许 Double-Check 的懒汉式是一个不错的选择。一个功能丰富的类库中,并非所有的类都会被使用。然而 ClassLoader 的加载机制,并不一定会将其排除至外。所以,一个懒汉式的单例有可能降低类库使用者的资源损耗。
一般来说,如果项目中不需要针对多线程情况的话,懒汉式、饿汉式的写法都适用;如果需要保证多线程并行使用推荐静态内部类和枚举
懒汉式与恶汉式对比
懒汉式单例是典型的时间换空间,每次取值都要时间做判断,判断是否需要创建实例,当然如果没有外部取值就不会创建对象,节约内存空间。
饿汉式单例是典型的空间换时间,类装载时就初始化实例,不管有没有访问取值,不需要做判断节约时间,如果一直没有外部访问取值就浪费了内存空间。
你知道懒加载吗?是怎么用在单例创建上的?有什么优势?
如果某个实例的创建 (比如数据库连接池的创建) 需要消耗很多系统资源,就需要引入懒加载机制。即上面的代码在类加载时就创建好了,如果在程序中始终没用到这个实例就会浪费很多系统资源。
为避免这种情况,就引入了懒加载机制,即在使用这个实例的时候才创建它。
参考资料
设计模式干货系列:(四)单例模式【学习难度:★☆☆☆☆,使用频率:★★★★☆】
单例模式各版本的原理与实践
【创建型模式四】单例模式(Singleton)
单例模式(详解,面试问题)
如何正确地写出单例模式
单例模式 - 如何简单的理解单例模式
Java 设计模式学习(一) - 单例模式
如何写线程安全的单例模式
Java 设计模式之单例模式
设计模式(三)——JDK 中的那些单例
网友评论