浅析设计模式-单例模式
定义
单例模式(Singleton Pattern):确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例,这个类称为单例类,它提供全局访问的方法。
现实世界模型:
比如古代一个国家只能有一个皇帝
单例模式需要满足以下特点:
- 单例类只有一个实例
- 单例类必须自己创建自己的唯一实例
- 单例类必须给所有其他对象提供这一实例
简单设计
单例模式的类图可以简单表示为:
单例模式-类图.png要正确的写好单例,需要关注两点:
- 多线程下的初始化是否是线程安全的
- 性能如何
主要分两种方法,懒汉和饿汉,区别在初始化唯一实例的时机。懒汉延迟初始化,饿汉在类加载的时候就初始化了
同时 Java 还有一个特殊的实现方式,使用枚举实现
懒汉
延迟加载单例,只有第一次使用的时候才进行实例化
懒汉需要做一些线程安全的处理
如果没有进行多线程处理,比如:
public class Singleton {
private static Singleton instance;
private Singleton (){}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
假设有两个线程,A 线程和 B 线程,两个线程同时调用 getInstance
当 A 线程执行 instance = new Singleton();
语句还没结束时,instance 还为 null
这时候 B 线程进入语句 if (instance == null)
,条件成立,也进入 instance = new Singleton();
这时候,这个类就会产生两个实例,都被外界拿去使用了
保证延迟加载并且线程安全的做法主要有这三种:
方法加同步
public class Singleton {
private static Singleton instance;
private Singleton (){}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
直接在 getInstance() 方法加同步,线程安全
缺点是调用 getInstance 比较密集的场景,同步方法频繁调用,性能低
静态内部类
public class Singleton {
private static class LazyHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton (){}
public static Singleton getInstance() {
return LazyHolder.INSTANCE;
}
}
利用 Java 的类加载机制,来延迟初始化对象实例。类被主动调用的时候,如果没有加载会执行加载
我们这里使用的是静态内部类,在没有被访问前,还没有进行加载。在使用 getInstance 方法后,该静态内部类被主动调用,于是开始了对这个类的类加载过程。
因为 Java 的类加载过程是同步的,包括类静态成员的初始化也是同步的,这个做法无需再单独加锁
ClassLoader 加载类的代码:
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
...
return c;
}
}
可见有对加载进行同步处理
双重检查锁定
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;
}
}
双重检查锁定和同步静态方法对比,把锁的粒度降低,所以只有在判断当前实例为空,才会进入被锁住的代码。这样子提高了使用的性能,保证在实例存在的情况下,不会因为互斥锁导致多个线程阻塞等待的现象
同时 volatile 关键字还确保了,虚拟机进行指令优化的时候,不会进行重排序导致对象延迟初始化
volatile 关键字的主要功能有三:
- 防止指令重排序
- 保证内存可见性
- 保证 long 或者 double 等基本类型的原子操作
如果没有加 volatile 关键字,由于虚拟机的指令排序优化,会把对象创建延迟到使用的时候。会出现 singleton 返回不会 null 的时候,但是还没有执行初始化对象实例,当另一个线程拿到对象实例,即使不为 null,但实际还是未初始化的对象
饿汉
在类加载后就实例化出一个静态对象出来,
静态工厂方法
public class Singleton {
private Singleton() {}
private static final Singleton singleton = new Singleton();
public static Singleton getInstance() {
return singleton;
}
}
饿汉始终是线程安全的
他的缺点后面即使没有使用单例,也会一直存在,某些场景下会耗费内存(比如某个进程一直都没有使用到那个单例,而这个单例持有大量内存,造成不必要的浪费)
枚举
public enum Singleton {
INSTANCE;
private Singleton() {
}
}
创建枚举默认是线程安全的,同时不需要担心反序列化导致重新创建新对象
应用
单例模式应用范围很广,可以在这里见到
控制实例的访问,需要访问一定在同一实例上进行的,比如 HttpClient
控制资源的使用,和线程池或者对象池配合使用,资源类型的单例,避免创建多个池浪费资源,节约内存
控制对象的创建,不需要多次重复创建,和工厂模式配合使用,一些工厂只需要进程中只保持一份,
优缺点
优势:
- 控制所有访问在唯一实例上进行
- 避免频繁创建和销毁对象带来的性能消耗
- 节省内存,避免可共享资源的重复创建
缺点:
- 如果职责过多,会导致单例类违背单一职责原则(拆分出其他模块来执行,单例做一个入口)
- 一些不必要的内存引用,会造成内存溢出(要有内存释放机制)
- 一些错误的引用,比如引用了不再使用的对象(典型的有对 Activity 的引用),会造成内存泄漏
使用注意
单例模式,实例一旦初始化,就会在内存中一直存在,要注意几点
- 不能滥用,要确定该实例是需要在整个进程中长期使用的
- 如果是资源单例的话,要考虑适当地释放一些很少使用的资源,避免页面增长造成内存泄漏
- 实例的成员变量,不要持有生命周期短的对象,如果持有要记得释放,否则会导致内存一直无法回收而造成内存泄漏
- 如果一个类是单例设计,不要用反射去生成它的实例,反射直接破坏对这个类的设计
- 使用懒汉式要注意线程安全