单例模式
定义: 确保一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。
类型: 创建类模式
类图:
单例模式特点
1、单例类只能有一个实例。
2、单例类必须自己创建自己的唯一实例。
3、单例类必须给所有其他对象提供这一实例。
单例模式应该是23种设计模式中最简单的一种模式了。它有以下几个要素:
- 私有的构造方法
- 指向自己实例的私有静态引用
- 以自己实例为返回值的静态的公有的方法
单例模式的实现方式
单例模式根据实例化对象时机的不同分为两种:
一种是饿汉式单例,一种是懒汉式单例。
饿汉式单例在单例类被加载时候,就实例化一个对象交给自己的引用;
懒汉式在调用取得实例方法的时候才会实例化对象。
1)饿汉式单例
是否 Lazy 初始化:否
是否多线程安全:是
实现难度:易
描述:这种方式比较常用,但容易产生垃圾对象。
优点:没有加锁,执行效率会提高。
缺点:类加载时就初始化,浪费内存。
/**
* 饿汉式单例
* <p>
* 饿汉式是典型的空间换时间,当类装载的时候就会创建类的实例,
* 不管你用不用,先创建出来,然后每次调用的时候,就不需要再判断,节省了运行时间。
*
* @author kaifeng
*/
public class HungrySingleton {
private static HungrySingleton hungrySingleton = new HungrySingleton();
/**
* 私有构造函数
*/
private HungrySingleton() {
}
/**
* 获取实例对象
*/
public static HungrySingleton getInstance() {
return hungrySingleton;
}
}
2)懒汉式单例
是否 Lazy 初始化:是
是否多线程安全:是
实现难度:易
描述:这种单例是典型的时间换空间,只有使用的时候才会实例化,但是每次获取实例的时候都会进行判断,是否已经实例,花费了一些判断时间,如果一直没有使用就不会实例化,节省了内存空间
优点:第一次调用才初始化,避免内存浪费。
缺点:必须加锁 synchronized 才能保证单例,但加锁会影响效率。
/**
* 懒汉式单例
* <p>
* 这种单例是典型的时间换空间,只有使用的时候才会实例化,但是每次获取实例的时候都会进行判断,
* 是否已经实例,花费了一些判断时间,如果一直没有使用就不会实例化,节省了内存空间
*
* @author kaifeng
*/
public class LazySingleton {
private static LazySingleton instance = null;
/**
* 私有构造函数
*/
private LazySingleton() {
}
/**
* 静态工厂方法,实例化对象
*/
public static synchronized LazySingleton getInstance() {
if (instance == null) {
instance = new LazySingleton();
}
return instance;
}
}
3)双检锁/双重校验锁(DCL,即 double-checked locking)
是否 Lazy 初始化:是
是否多线程安全:是
实现难度:较复杂
描述:这种方式采用双锁机制,安全且在多线程情况下能保持高性能。
/**
* 双检锁/双重校验锁(DCL,即 double-checked locking)
*
* @author kaifeng
*/
public class DCLSingleton {
private volatile static DCLSingleton instance;
private DCLSingleton() {
}
public static DCLSingleton getInstance() {
//第一次检查实例是否存在,如果不存在才进入下面的同步块,否则直接返回实例
if (instance == null) {
//同步代码块,线程安全的创建实例
synchronized (DCLSingleton.class) {
//第二次检查实例是否存在,如果不存在才真正的创建实例
if (instance == null) {
instance = new DCLSingleton();
}
}
}
return instance;
}
}
所谓“双重检查加锁”机制:并不是每次进入getInstance方法都需要同步,而是先检查实例是否存在,如果不存在才进行下面的同步块,这是第一重检查,进入同步块过后,再次检查实例是否存在,如果不存在,就在同步的情况下创建一个实例,这是第二重检查。这样一来,就只需要同步一次了,从而减少了多次在同步情况下进行判断所浪费的时间。
“双重检查加锁”机制的实现会使用关键字volatile,被volatile修饰的变量的值,将不会被本地线程缓存,所有对该变量的读写都是直接操作共享内存,从而确保多个线程能正确的处理该变量。
4)Initialization-on-demand holder idiom
是否 Lazy 初始化:是
是否多线程安全:是
实现难度:一般
描述:这种方式能达到双检锁方式一样的功效,但实现更简单。对静态域使用延迟初始化,应使用这种方式而不是双检锁方式。这种方式只适用于静态域的情况,双检锁方式可在实例域需要延迟初始化时使用。
/**
* 在这个类级内部类里面去创建对象实例,只要不使用到这个类级内部类,
* 那就不会创建对象实例,从而同时实现延迟加载和线程安全。
*
* @author kaifeng
*/
public class Singleton {
private Singleton() {
}
/**
* 类级的内部类,就是静态的成员式内部类,该内部类的实例与外部类的实例
* 没有绑定关系,而且只有被调用时才会装载,从而实现了延迟加载。
*/
private static class SingletonHolder {
/**
* 静态初始化器,由JVM来保证线程安全
*/
private static Singleton instance = new Singleton();
}
public static Singleton getInstance() {
return SingletonHolder.instance;
}
}
当getInstance方法第一次被调用的时候,第一次读取SingletonHolder.instance,导致SingletonHolder类得到初始化;而这个类在装载并被初始化的时候,会初始化它的静态域,从而创建Singleton的实例,由于是静态的域,因此只会在虚拟机装载类的时候初始化一次,并由虚拟机来保证它的线程安全性。
这个模式的优势在于,getInstance方法并没有被同步,并且只是执行一个域的访问,因此延迟初始化并没有增加任何访问成本。
5)枚举
是否 Lazy 初始化:否
是否多线程安全:是
实现难度:易
描述:这种实现方式还没有被广泛采用,但这是实现单例模式的最佳方法。它更简洁,自动支持序列化机制,绝对防止多次实例化。
这种方式是 Effective Java 作者 Josh Bloch 提倡的方式,它不仅能避免多线程同步问题,而且还自动支持序列化机制,防止反序列化重新创建新的对象,绝对防止多次实例化。不过,由于 JDK1.5 之后才加入 enum 特性,用这种方式写不免让人感觉生疏,在实际工作中,也很少用。
不能通过 reflection attack 来调用私有构造方法。
/**
* @author kaifeng
*/
public enum EnumSingleton {
/**
* 定义一个枚举的元素,它就代表了Singleton的一个实例
*/
instance;
/**
* 单例可以有自己的操作
*/
public void singletonCustom() {
//自定义操作
}
}
单例模式的优点:
- 在内存中只有一个对象,节省内存空间。
- 避免频繁的创建销毁对象,可以提高性能。
- 避免对共享资源的多重占用。
- 可以全局访问。
适用场景:
- 需要频繁实例化然后销毁的对象。
- 创建对象时耗时过多或者耗资源过多,但又经常用到的对象。
- 有状态的工具类对象。
- 频繁访问数据库或文件的对象。
- 系统只需要一个实例对象,如系统要求提供一个唯一的序列号生成器
单例模式注意事项:
- 只能使用单例类提供的方法得到单例对象,不要使用反射,否则将会实例化一个新对象。
- 不要做断开单例类对象与类中静态引用的危险操作。
- 多线程使用单例使用共享资源时,注意线程安全问题。