1. 单例模式介绍
案例:皇帝,中国历朝历代,很少出现两个皇帝并存的时候,我们把皇帝看作是一个单例,在一位皇帝即位期间,大臣朝拜的皇帝应该只能是一个。
类图:
代码:
/**
* @Description 皇帝
*/
public class Emperor {
// 定义一个皇帝
private static Emperor emperor = null;
private Emperor() {
// 私有构造器,目的就是不能让客户端通过new的方式创建一个皇帝对象
}
// 获取皇帝对象只能通过对外暴露的这个方法
public static Emperor getInstance() {
// 如果皇帝还没有定义,那就定一个
if(emperor == null) {
emperor = new Emperor();
}
return emperor;
}
// 打印皇帝名字
public static void emperorInfo() {
System.out.println("康熙大帝...");
}
}
/**
* @Description 大臣
*/
public class Minister {
public static void main(String[] args) {
// 第一天
Emperor emperor1 = Emperor.getInstance();
emperor1.emperorInfo();
// 第二天
Emperor emperor2 = Emperor.getInstance();
emperor2.emperorInfo();
// 第二天
Emperor emperor3 = Emperor.getInstance();
emperor3.emperorInfo();
// 无论什么时候朝拜皇帝,都是同一个人
}
}
以上代码是一个最简单的单例模式的实现,即将构造器私有化,提供唯一的获取对象实例的方法,保证每个访问的都是同一个对象。这种实现方式简单,但未考虑多线程环境下多次创建对象等其他问题。
2. 5种单例模式
2.1 饿汉式单例模式
public class HungerSingleton {
private static HungerSingleton instance = new HungerSingleton();
private HungerSingleton() {}
public static HungerSingleton getInstance() {
return instance;
}
}
- 特点:static 变量会在类装载时初始化,此时也不会涉及多个线程对象访问该对象的问题,虚拟机保证只会装载一次该类,肯定不会发生并发访问的问题,因此可以省略 synchronized 关键字
- 问题:如果只是加载了本类,而没有调用 getInstance() 方法,那么这个单例对象存在就没有意义,造成资源浪费
2.2 懒汉式单例模式
public class LazySingleton {
private static LazySingleton instance;
private LazySingleton() {}
public static synchronized LazySingleton getInstance() {
if(instance == null) {
instance = new LazySingleton();
}
return instance;
}
}
- 特点:lazy load,延迟加载,懒加载,真正用的时候才加载,提高了资源利用率
- 问题:每次调用getInstance()方法都要同步,并发效率较低
2.3 双重检测锁式单例模式
public class DoubleCheckSingleton {
private static DoubleCheckSingleton instance;
private DoubleCheckSingleton() {}
public static DoubleCheckSingleton getInstance() {
if (instance == null) {
DoubleCheckSingleton singleton;
synchronized (DoubleCheckSingleton.class) {
singleton = instance;
if (singleton == null) {
synchronized (DoubleCheckSingleton.class) {
if (singleton == null) {
singleton = new DoubleCheckSingleton();
}
}
instance = singleton;
}
}
}
return instance;
}
}
- 特点:将同步内容写到 if() 内部,提高了并发执行的效率,不必每次获取对象都进行同步,只有第一次才同步,创建以后就没有必要了
- 问题:由于编译器优化和JVM底层内部模型原因,偶尔会出现问题。不建议使用
2.4 静态内部类式单例模式
public class StaticInnerClassSingleton {
private StaticInnerClassSingleton() {}
private static class StaticInnerClassSingletonInstance {
private static final StaticInnerClassSingleton instance = new StaticInnerClassSingleton();
}
public static StaticInnerClassSingleton getInstance() {
return StaticInnerClassSingletonInstance.instance;
}
}
- 特点:外部类没有 static 属性,不会像饿汉式那样立即加载,只有真正调用 getInstance() 方法,才会加载静态内部类,加载类时是线程安全的,instance 是 static final 类型,保证了内存中只有这样一个实例存在而且只能被赋值一次,从而保证了线程安全性,兼顾了并发高效调用和延迟加载的优势
2.5 枚举单例
public enum EnumSingleton {
// 定义一个枚举的元素,它就代表了 EnumSingleton 的一个实例
INSTANCE;
public static void main(String[] args) {
// 需要的时候直接调用就可以
EnumSingleton instance = EnumSingleton.INSTANCE;
// 其他操作...
}
}
- 特点:实现简单,枚举本身就是单例模式,由 JVM 从根本上提供保障,避免反射和序列化的漏洞
- 问题:不能延迟加载
2.6 5种单例模式的比较
| 线程安全 | 延迟加载 | 性能 | 能否抵御反射和序列化的破解 | 其他 | |
|---|---|---|---|---|---|
| 饿汉式 | ✅ | ❌ | 高 | ❌ | |
| 懒汉式 | ✅ | ✅ | 低 | ❌ | |
| 双重检测锁 | ✅ | ✅ | 低 | ❌ | 由于JVM底层内部模型原因,偶尔会出问题 |
| 静态内部类 | ✅ | ✅ | 高 | ❌ | |
| 枚举 | ✅ | ❌ | 高 | ✅ |
如何选择单例模式:
- 单例对象占用资源少,不需要延时加载:枚举式好于饿汉式
- 单例对象占用资源多,需要延时加载:静态内部类式好于懒汉式和双重检测锁式
3. 破解单例模式
3.1 使用反射破解单例模式
import java.lang.reflect.Constructor;
public class CrackSingletonByReflectDemo {
private static CrackSingletonByReflectDemo instance = new CrackSingletonByReflectDemo();
private CrackSingletonByReflectDemo() {
}
public static CrackSingletonByReflectDemo getInstance() {
return instance;
}
public static void main(String[] args) throws Exception {
CrackSingletonByReflectDemo instance1 = CrackSingletonByReflectDemo.getInstance();
CrackSingletonByReflectDemo instance2 = CrackSingletonByReflectDemo.getInstance();
// true
System.out.println(instance1 == instance2);
// 通过反射的方法破解单例模式
Class<CrackSingletonByReflectDemo> clazz = (Class<CrackSingletonByReflectDemo>) Class.forName("com.yunhe.dp.ydpodp.p03_singleton_pattern.CrackSingletonByReflectDemo");
// 获得无参构造器
Constructor<CrackSingletonByReflectDemo> constructor = clazz.getDeclaredConstructor(null);
// 设置跳过权限检查,可以访问私有成员
constructor.setAccessible(true);
CrackSingletonByReflectDemo instance3 = constructor.newInstance();
CrackSingletonByReflectDemo instance4 = constructor.newInstance();
// false
System.out.println(instance3 == instance4);
}
}
解决办法:改写私有的构造方法
private CrackSingletonByReflectDemo() {
if (instance != null) {
throw new RuntimeException("instance已经实例化!");
}
}
3.2 通过反序列化破解单例模式
public class CrackSingletonBySerializableDemo implements Serializable {
private static CrackSingletonBySerializableDemo instance;
private CrackSingletonBySerializableDemo() {}
public static synchronized CrackSingletonBySerializableDemo getInstance() {
if (instance == null) {
instance = new CrackSingletonBySerializableDemo();
}
return instance;
}
public static void main(String[] args) throws Exception {
CrackSingletonBySerializableDemo instance1 = CrackSingletonBySerializableDemo.getInstance();
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("instance.txt"));
out.writeObject(instance1);
out.close();
ObjectInputStream in = new ObjectInputStream(new FileInputStream("instance.txt"));
CrackSingletonBySerializableDemo instance2 = (CrackSingletonBySerializableDemo) in.readObject();
// false
System.out.println(instance1 == instance2);
in.close();
}
}
解决办法:在类中添加readResolve()这个方法,反序列化直接调用readResolve()返回指定的对象,不需要再创建新对象
public Object readResolve() {
return instance;
}
本文第1节原书:《您的设计模式》作者:CBF4LIFE