第四章 单例模式 - 确保对象的唯一性
单例模式用于确保对象的唯一性,为了确保系统中某一个类只有一个唯一实例,当这个唯一实例创建后,无法再创建一个同类型的其他对象。
一、概述
单例模式:确保一个类只有一个实例,而且自行实例化并向整个系统提供这个实例,这个类称为单例类,它提供全局访问的方法。属于对象创建型模式。
1.2 单例模式中的角色
- Singleton - 单例角色
- 在单例类内容实现只生成一个实例,同时提供一个静态工厂方法用于获取该唯一实例
- 为了防止外部实例化,将其构造函数设计为私有
- 单例类内部顶一个Singleton类型的静态对象,作为外部共享的唯一实例
二、懒汉模式与饿汉模式
2.1 饿汉式单例类 - 线程安全
class EagerSingleton{
private static final EagerSingleton instance = new EagerSingleton();
private EagerSingleton(){}
public static EagerSingleton getInstance(){
return instance;
}
}
饿汉模式实现起来最为简单,在类加载时,静态变量 instance 就会被初始化,此时类的私有构造函数会被调用,单例类的唯一实例将被创建。
2.2 懒汉式单例类(一) - 线程安全
懒汉模式能将类进行 延迟加载,即在需要使用时再加载实例,为了避免多线程调用 getInstance() 时实例化多个 LazySingleton,这里使用 synchronized 关键字进行加锁。
class LazySingleton{
private static LazySingleton instance = null;
private LazySingleton(){}
synchronized public static LazySingleton getInstance(){
if(instance == null)
instance = new LazySingleton();
return instance;
}
}
2.3 懒汉式单例类(二) - 线程不安全
虽然 synchronized 关键字进行了线程锁定,但是每次调用 getInstance() 方法时都会进行加锁操作,在多线程高并发场景下,会导致系统性能大大降低。
事实上,只需要在 new L:azySingleton() 时进行加锁即可,即第一次实例化实例时加锁。
public static LazySingleton getInstance(){
if(instance == null){
synchronized(LazySingleton.class){
instance = new LazySingleton();
}
}
return instance;
}
2.4 懒汉模式单例类(三 DoubleCheck) - 线程不安全
如果当两个线程同时进行了 if 条件之中,当线程A完成实例化之后,线程B继续执行,依旧会进行一次实例化。违背了单例模式的设计思想。
双重检测机制:因此我们需要在 synchronized 中在进行一次判断
public static LazySingleton getInstance(){
if(instance == null){
synchronized(LazySingleton.class){
if(instance == null){
instance = new LazySingleton();
}
}
}
return instance;
}
2.5 懒汉模式单例类(四 volatile+DoubleCheck) - 线程安全
上述代码看起来可能没什么问题,但是
instance = new LazySingleton() 在内存中的指令顺序应该为:
- instance 分配内存地址
- 实例化 LazySingleton
- 将实例化地址指向instance
因为 JVM 的指令重排和优化,可能会导致以上执行顺序与所想像的不一致。最终导致单例出现问题,因此我们在静态实例 instance 前,加上 volatile 关键词,避免 instance 的实例化发生指令重排。
因此
完整的懒汉模式
如下:
class LazySingleton{
// 使用volatile 修改时静态实例,避免指令重排序
private volatile static LazySignleton instance = null;
// 构造方法私有化,防止外部实例
private LazySingleton(){}
// 静态工厂类,用于返回唯一实例,且使用延迟加载技术
public static LazySingleton getInstance(){
// 第一重检查,只有当instance 为空时才进行实例化
if(instance == null){
// 实例化时对当前类进行加锁,避免多线程同时实例化
synchronized(LazySingleton.class){
// 第二重检查,避免当前线程通过第一重检查后其他线程已实例化过
if(instance == null){
instance = new LazySingleton();
}
}
}
return instance;
}
}
缺点也很明显:1. volatile 关键词虽然能保证实例避免重排,但是只在1.5以上的版本才能生效 2. volatile关键词会屏蔽 JVM 的一些代码优化,可能导致系统运行效率降低
三、一种更好的单例实现方法
3.1 懒汉模式与饿汉模式的缺点
饿汉模式:不能实现延迟加载,不管实例是否使用都将占据内存
饿汉模式:繁琐的线程安全控制,且性能会受到一定影响
3.2 静态内部类单例 - IoDH - 线程安全
class Signleton{
private Singleton(){}
// 静态内部类只有在第一次调用时才会进行加载
private static class HolderClass{
// 因为instance 不是Singleton的成员变量,所以不会在Singleton加载时实例化
private final static Singleton instance = new Singleton();
}
// 通过JVM特性来保证线程安全性,确保成员变量只能初始化一次
public static Singleton getInstance(){
return HolderClass.instance;
}
}
Initialization Demand Holder 技术,在单例类内部增加一个静态内部类,在该内部类中创建单例对象,再将单例对象通过 getInstance() 方法返回给外部使用。
但是IoDH依赖于JVM的特性,并不适用于所有语言。但是IoDH既可以实现延迟加载,又可以保证线程安全,又不影响性能,不失为一种最好的Java语言单例模式。
四、总结
优点
- 单例模式提供了对唯一实例的受控访问。因为单例类封装了它的唯一实例,严格控制客户如何访问
- 节约系统资源。因为内存中只存在一个对象,无需频繁的创建、销毁对象
- 允许可变数目的实例。可以通过变种,让getInstance返回至多几个实例
缺点
- 由于单例类没有抽象层,很难进行扩展
- 单例类职责过重,违背了“单一职责原则”。因为单例类即是工厂角色,提供工厂方法,又是产品角色,包含一些业务方法。将产品的创建和产品的功能融合到了一起
- 许多面向对象语言(如Java、C#)的自动垃圾回收技术,如果实例化的共享变量长时间不被使用。GC 会自动销毁并回收资源,导致单例对象状态的丢失。
适用场景
- 系统只需要一个实例对象。如资源消耗过大的对象、资源管理器等只能实例一次的对象
- 客户调用类的单个实例只允许使用一个公共访问点,不能通过其他途径访问该实例。