设计模式六之单例模式
有些系统中,为了节省内存资源,保证数据内容的一致性,对某些类要求只能创建一个实例,这就是所谓的单例模式。
1. 模式的定义与特点
1.1 模式的定义
单例模式(Singleton):一个类只有一个实例,且该类能自行创建这个实例的一种模式。在计算机系统中,多线程中的线程池、应用程序的日志对象、数据库的连接池、网站的计数器、Web 应用的配置对象和系统中的缓存等常常被设计成单例模式。
1.2 模式的特点
单例模式的特点有:
1. 单例类只有一个实例;;
2. 该单例对象必须由该单例类自行创建;
2. 单例类对外提供一个访问该单例类的全局访问点。
2. 模式的结构与实现
单例模式是设计模式中最简单的模式之一,通常,普通类的构造函数都是公有的,外部类可以通过 new 构造函数 来生成多个实例。但是,如果该类的构造函数设为私有的,外部类就无法调用该构造函数,也就无法生成多个实例。这时候该类就必须定义一个静态私有实例,并向外部提供一个静态公有的函数用于创建或获取该静态私有实例。
2.1 模式的结构
单例模式的主要结构为:
1.单例类:包含一个实例且能自行创建这个实例的类;
2. 访问类:使用单例的类;
2.2 模式的实现
单例模式的实现主要考虑到以下几点:私有构造器、线程安全、延迟加载、序列化和反序列化安全和反射。
2.2.1 懒汉模式
/**
* 懒加载单例
*/
public class LazySingleton implements Serializable {
/**
* 静态私有实例
*/
private static LazySingleton lazySingleton = null;
private LazySingleton() {
}
/**
* 创建或获取静态私有实例的公有静态函数
* @return
*/
public synchronized static LazySingleton getInstance() {
if (lazySingleton == null) {
lazySingleton = new LazySingleton();
}
return lazySingleton;
}
/**
* 防止序列化和反序列化破坏单例,单例类必须实现 Serializable 序列化接口
* @return
*/
public Object readResolve() {
return instance;
}
}
该单例懒汉模式提供了一个私有静态实例和一个创建或获取私有实例的公有方法以及一个私有的构造器,同时满足延迟加载,但是使用 Synchronized 修饰获取实例的方法,性能不佳,因此需要进一步的优化。
2.2.2 双重检查(Double-Check)
/**
* 懒加载双重检查单例
*/
public class LazyDoubleCheckSingleton implements Serializable {
/**
* 静态私有实例且用volatile修饰保证可见性
*/
private volatile static LazyDoubleCheckSingleton lazyDoubleCheckSingleton = null; 1
private LazyDoubleCheckSingleton() {
}
/**
* 创建或获取静态私有实例的公有静态函数
* @return
*/
public static LazyDoubleCheckSingleton getInstance() {
if (lazyDoubleCheckSingleton == null) {
synchronized (LazyDoubleCheckSingleton.class) {
if (lazyDoubleCheckSingleton == null) {
lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton(); 2
}
}
}
return lazyDoubleCheckSingleton;
}
/**
* 防止序列化和反序列化破坏单例,单例类必须实现 Serializable 序列化接口
* @return
*/
public Object readResolve() {
return instance;
}
}
使用双重检查实现懒加载单例有两个点需要注意:1 为什么需要两个 if 判断 2 为什么需要 实现 volatile 修饰实例变量?这里需要说明一下。
使用两个 if 双重检查是因为两个线程同时进入 getInstance() 方法,都要进行 instantce == null 的判断,然后有线程1获取了 synchronized 锁 开始执行 代码 2 (即23行),进行对象实例的创建后释放锁,此时线程2如果不再次进行 instantce == null 的判断,线程2就会再次进行对象实例的创建,因此 代码23行处的判处是非常有必要的。
使用 volatile 修饰实例变量是为了防止重排序,保证可见性,代码23行处共执行3条命令:1 分配对象的内存空间;2 初始化对象;3 设置变量指向刚刚分配的内存地址。但是在 步骤2 和步骤3 之间顺序不固定,有时候步骤2 先执行,有时候步骤3 先执行,因此如果线程1先执行步骤3就释放锁,线程2 判断 instance != null 后直接返回的就是空对象,因此需要使用 volatile 防止重排序,保证可见性。想要深入的了解 volatile 关键字的是如何保证可见性和有序性,可以参考《并发编程之 volatile 关键字 - 解决可见行和有序性》
声明为volatile的变量可以做到一下保证:
1. 其他线程对变量的修改,可以及时的反应在当前线程中。
2. 确保当前线程对变量的修改,能及时写回共享内存中,并被其他线程所见
3. 使用volatile声明的变量,编译器会保证其有序性。
2.2.3 静态内部类
/**
* 静态内部类
*/
public class StaticInnerClassSingleton implements Serializable {
private StaticInnerClassSingleton() {
}
public static StaticInnerClassSingleton getInstance() {
return InnerClass.staticInnerClassSingleton;
}
/**
* 静态内部类,创建单例类实例
*/
private static class InnerClass {
private static StaticInnerClassSingleton staticInnerClassSingleton = new StaticInnerClassSingleton();
}
/**
* 防止序列化和反序列化破坏单例,单例类必须实现 Serializable 序列化接口
* @return
*/
public Object readResolve() {
return instance;
}
}
在双重检查机制中,使用 volatile 防止重排序,保证内存可见性。使用静态内部类是在创建对象时,对象初始化和变量指向内存地址顺序变化时,对线程不可见。这种方式利用了 classloader 机制来保证初始化 instance 时只有一个线程,Singleton 类被装载了,instance 不一定被初始化。因为 InnerClass 类没有被主动使用,只有通过显式调用 getInstance 方法时,才会显式装载 InnerClass 类,从而实例化 instance,同时确保了 intance 实例延迟加载。
2.2.4 饿汉式单例
/**
* 饿汉式单例
*/
public class HungrySingleton implements Serializable {
/**
* 静态不变实例
*/
private final static HungrySingleton instance = new HungrySingleton();
/**
* 私有构造器
*/
private HungrySingleton() {
}
/**
* 静态方法提供获取单例类实例
* @return
*/
public static HungrySingleton getInstance() {
return instance;
}
/**
* 防止序列化和反序列化破坏单例,单例类必须实现 Serializable 序列化接口
* @return
*/
public Object readResolve() {
return instance;
}
}
饿汉式单例比较简单,就是在类初始化的时候创建实例,因此没有延迟加载,在实例加载过程复杂的情况下,类一直没有被使用,就会造成内存浪费。
2.2.5 防止序列化和反序列化破坏单例
public class Client {
public static void main(String[] args) throws Exception {
HungrySingleton instance = HungrySingleton.getInstance();
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton_file"));
oos.writeObject(instance);
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("singleton_file"));
HungrySingleton hungrySingleton = (HungrySingleton) ois.readObject();
System.out.println(instance);
System.out.println(hungrySingleton);
System.out.println(instance == hungrySingleton);
}
}
运行结果如下:
com.baidu.test.disign.creational.singleton.HungrySingleton@d716361
com.baidu.test.disign.creational.singleton.HungrySingleton@34c45dca
false
在使用对象流进行写入和读取的时候会发现,单例类被破坏了,写入和读取的对象发生了改变,这就违反了单例只有一个实例的原则。现在我们就来防止序列化和反序列化破坏单例。我们已饿汉式举例:
/**
* 饿汉式单例,防止序列化破坏
*/
public class HungrySingleton implements Serializable {
/**
* 静态不变实例
*/
private final static HungrySingleton instance = new HungrySingleton();
/**
* 私有构造器
*/
private HungrySingleton() {
}
/**
* 静态方法提供获取单例类实例
* @return
*/
public static HungrySingleton getInstance() {
return instance;
}
/**
* 防止序列化和反序列化破坏单例,单例类必须实现 Serializable 序列化接口
* @return
*/
public Object readResolve() {
return instance;
}
}
# 再次执行 client 类,其执行结果为:
com.baidu.test.disign.creational.singleton.HungrySingleton@d716361
com.baidu.test.disign.creational.singleton.HungrySingleton@d716361
true
在这里我们会发现,单例类实现 Serializable 序列化接口,然后在添加一个 readResolve 方法,其得到的单例类永远就式一个,这是为什么呢?同时为什么就是 readResolve 方法呢?下面我们来看一下:
public class ObjectInputStream
extends InputStream implements ObjectInput, ObjectStreamConstants
{
public final