23种设计模式之单例模式(Singleton Pattern)
创建型设计模式
意图: 类负责创建自己的对象,同时确保只有单个对象被创建,并且该类提供一种访问其唯一对象的方式,可以直接访问,不需要实例化。
关键代码: 构造函数私有。提供唯一一个获取对象的方法。
主要解决:一个全局使用的类频繁的创建和销毁
何时使用:当你想控制实例数目,节省系统资源的时候。
优点:
- 在内存中只有一个实例,减少了内存的开销(避免频繁的创建和销毁对象)
缺点:
- 没有接口,不能继承,与单一职责原则冲突,一个类应该只关心内部逻辑,而不关心外面怎么样来实例化。
- 长时间不使用实例对象,系统会认为该对象是垃圾对象而被回收。可能会造成对象状态的丢失。
注意:
- 单例只能有一个实例
- 单例类必须自己创建自己的唯一实例
- 单例类必须给所有其他对象提供这一实例
1. 单例模式的几种实现方式:
1.1 . 懒汉式
懒加载,多线程的情况下不安全。
优点: 对象使用时再进行加载,节省内存
缺点: 多线程情况下不安全
// 懒加载多线程不安全方式:
public class Singleton {
private static Singleton instance;
private Singleton (){}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
// 懒加载多线程安全方式:
public class Singleton {
private static Singleton instance;
private Singleton (){}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
1.2 . 饿汉式
类加载时就创建对象,多线程安全
缺点: 类加载时就初始化,浪费内存
优点: 没有加锁,执行效率会很高。
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton (){}
public static Singleton getInstance() {
return instance;
}
}
1.3. 双重检查锁\双重校验锁(DCL)
懒加载,多线程安全。
/***
* 第一次 if (singleton == null) 是为了不进入 synchronized ,提高性能
*
*第二次 if (singleton == null) 是为了不生成多个实例。
*
*/
public class Singleton {
private volatile static Singleton singleton;
private Singleton (){}
public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
双重检查锁中的volatile存在的意义:
volatile 保证有序性和禁止指令重排
why?: 使用javap -c 生成字节码文件,通过观察可以发现,创建一个对象实例的步骤不是一个原子性的操作,可以分为三步
1. 分配对象内存
2. 调用构造器方法,执行初始化
3. 将对象引用赋值给变量
两个线程,线程A 和 线程B 同时执行
当线程执行到 new Singleton 时,发生了指令重排序,执行了 分配对象内存,将对象引用赋值给变量,还没有来及执行 构造器的方法进行初始化,此时线程B 进入代码,进行 singleton 对象的 非null 校验,此时由于线程A 的作用 singleton 对象已经不为 Null 但是,对象中的属性值还没有被赋值,此时 线程 虽然拿到一个非 null 的对象,但是属性值还没有赋值好,使用起来会 发生异常。。。
synchronized 提供了有序性的保证,为什么还需要添加 volatile 关键字呢?
synchronized保证的有序性是多个线程之间的有序性,即被加锁的内容要按照顺序被多个线程执行。但是其内部的同步代码还是会发生重排序。
2. 破坏懒汉式单例和饿汉式单例
无论是完美的懒汉式还是饿汉式,通过反射和序列化都能破坏单例模式。
/**
* 通过反射,强制访问类的私有构造器,去创建另一个对象
*/
public static void main(String[] args) {
// 获取类的显式构造器
Constructor<Singleton> construct = Singleton.class.getDeclaredConstructor();
// 可访问私有构造器
construct.setAccessible(true);
// 利用反射构造新对象
Singleton obj1 = construct.newInstance();
// 通过正常方式获取单例对象
Singleton obj2 = Singleton.getInstance();
System.out.println(obj1 == obj2); // false
}
/**
* 通过序列化和反序列化破坏单例模式
*/
public static void main(String[] args) {
// 创建输出流
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("Singleton.file"));
// 将单例对象写到文件中
oos.writeObject(Singleton.getInstance());
// 从文件中读取单例对象
File file = new File("Singleton.file");
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
Singleton newInstance = (Singleton) ois.readObject();
// 判断是否是同一个对象
System.out.println(newInstance == Singleton.getInstance()); // false
}
3. 枚举的方式防止单例模式被破坏
以上介绍了通过反射和序列化/反序列化可以破坏掉单例模式,接下来介绍一种无法破坏掉的创建单例方式。(枚举(枚举在jdk1.5后引进的))
public enum Singleton {
INSTANCE;
public void doSomething() {
System.out.println("这是枚举类型的单例模式!");
}
}
4. 枚举方式创建单例的三种优势:(单元素的枚举类型)
- 代码简洁
- 天然的线程安全与单一实例
- 能保证单例模式不被破坏
4.1 天然的线程安全与单一实例
/**
* 在程序 启动时,会调用Singleton 的空参构造器,实例好一个 Singleton 对象赋给Instance,之后再也不会实例化。
*/
public enum Singleton {
INSTANCE;
Singleton() { System.out.println("枚举创建对象了"); }
public static void main(String[] args) { /* test(); */ }
public void test() {
Singleton t1 = Singleton.INSTANCE;
Singleton t2 = Singleton.INSTANCE;
System.out.print("t1和t2的地址是否相同:" + t1 == t2);
}
}
//输出
// 枚举创建对象了
// t1和t2的地址是否相同:true
4.2 枚举是如何保证单例模式不被破坏的
通过反射获取构造器,调用 newInstance()方法是,会判断是否是枚举类型,枚举类型会直接抛出异常。
序列化读取对象时,writeObject()方法,每个枚举类型和枚举姓名都是唯一的,所以在序列化时,仅仅只是对枚举的类型和变量名输出到文件中,在读入文件反序列化成对象时,使用Enum类的valueOf(String name)方法根据变量的名字查找对应的枚举对象。(所以,在序列化和反序列化的过程中,只是写出和读入了枚举类型和名字,没有任何关于对象的操作。)
public class ObjectOutputStream
extends OutputStream implements ObjectOutput, ObjectStreamConstants
{
public final void writeObject(Object obj) throws IOException {
if (enableOverride) {
writeObjectOverride(obj);
return;
}
try {
writeObject0(obj, false);
} catch (IOException ex) {
if (depth == 0) {
writeFatalException(ex);
}
throw ex;
}
}
private void writeObject0(Object obj, boolean unshared)
throws IOException
{
boolean oldMode = bout.setBlockDataMode(false);
depth++;
try {
// handle previously written and non-replaceable objects
int h;
if (
...
} else if (obj instanceof Class) {
writeClass((Class) obj, unshared);
return;
} else if (obj instanceof ObjectStreamClass) {
...
}
...
// remaining cases
if (obj instanceof String) {
...
} else if (obj instanceof Enum) {
writeEnum((Enum<?>) obj, desc, unshared);
} else if (obj instanceof Serializable) {
...
}
} finally {
...
}
}
}
枚举类不需要关注线程安全、破坏单例和性能问题,因为其创建对象的时机与饿汉式单例有异曲同工之妙。