共计 4687 个字符,预计需要花费 12 分钟才能阅读完成。
单例模式是一种常用的设计模式、也可能是设计模式中代码量最少的设计模式。但是少并不意味着简单、想要用好、用对单例、就的费一番脑子了。因为它里面涉及到了很多 Java 底层的知识如类装载机制、Java 内存模型、volatile 等知识点。
简介
单例模式属于 23 中设计模式中的创建型模式、定义是确保某一个类只有一个实例、并提供一个全局的访问点。
具有以下 3 个特性:
- 只能有一个实例
- 必须自己创建自己唯一实例
- 提供全局访问点
基本实现思路
单例要求类只能返回同一对象的引用、必须提供一个静态获取该实例的方法
实现可以通过以下两步:
- 私有化构造方法、防止外部实例化、只有通过对外提供的静态方法来获取唯一实例
- 提供一个静态方法获取对象的实例。
单例的 7 种实现方式
1. 饿汉式
public class EagetSingleton {private static final EagetSingleton INSANCE = new EagetSingleton();
// 私有化构造函数、防止外部实例化
private EagetSingleton() {}
// 提供静态外部访问方法
public static EagetSingleton getInstance() {return INSANCE;}
}
优点:写法简单、类装载时就实例化了静态变量、避免了线程并发问题。
缺点:在类装载过程中就实例化了对象、造成了资源浪费。
2. 饿汉式(静态代码块)
public class StaticBlockSingleton {
private static StaticBlockSingleton INSTANCE = null;
static {
try {INSTANCE = new StaticBlockSingleton();
} catch (Exception e) {}}
// 私有化构造函数、防止外部实例化
private StaticBlockSingleton() {}
// 提供静态外部访问方法
public static StaticBlockSingleton getInstance() {return INSTANCE;}
}
这种方式和上述实现方式基本相同、只是把类实例化的过程放到了静态代码块中来实例化、同样也是在类装载过程执行静态代码块、优缺点基本相同但是它可以在类实例化过程中做一些额外的操作如异常处理等。
3. 懒汉式(线程不安全)
public class LazySingleton {
private static LazySingleton INSTANCE = null;
// 私有化构造函数、防止外部实例化
private LazySingleton() {}
// 提供静态外部访问方法
public static LazySingleton getInstance() {if (null == INSTANCE) { -------- 1
INSTANCE = new LazySingleton(); ------2}
return INSTANCE;
}
}
优点:实现了懒加载、避免了资源的浪费。
缺点:线程不安全、在多线程情况下当一个线程执行到 1 处的时候、还没有来得及往下执行另一个线程也到 1 处 这样两个线程同时执行 2 处代码、破坏了单例。
4. 懒汉式(加锁)
public class LazySyncSingleton {
private static LazySyncSingleton INSTANCE = null;
// 私有化构造函数、防止外部实例化
private LazySyncSingleton() {}
// 效率低下
// 提供静态外部访问方法
public static synchronized LazySyncSingleton getInstance() {if (null == INSTANCE) {INSTANCE = new LazySyncSingleton();
}
return INSTANCE;
}
}
解决了 3 中线程不安全的问题、利用 synchronized 对 getInstance()方法加锁以达到同步访问。
优点:线程同步
缺点:效率低下、此方式对整个对象加锁、每次访问 getInstance() 都需要同步访问、这种情况多线程并发效率非常低下、其实我们只需要在对象还没实例化前加锁就可以了、实例化后就不存在并发问题了。
5. 懒汉式(双重锁)
public class DCheckSingleton {
private static volatile DCheckSingleton INSTANCE = null;
// 私有化构造函数、防止外部实例化
private DCheckSingleton() {}
// 提供静态外部访问方法
public static DCheckSingleton getInstance() {if (null == INSTANCE) {synchronized (DCheckSingleton.class) {if (null == INSTANCE) {INSTANCE = new DCheckSingleton();
}
}
}
return INSTANCE;
}
}
解决了 4 中并发情况下效率低下的问题。
优点:线程安全、延迟加载、效率高
涉及到知识点:1:volatile 关键字 确保内存的可见性和有序性。如果不加 volatile 关键字会有什么情况?我知道在对象实例化时 INSTANCE = new DCheckSingleton(); 这一句代码 JVM 中并不是一步执行的而是分为三步(1)在栈内存中为 创建对象的引用指针 INSTANCE(2)在堆内存中开辟一块空间来存放实例化的对象 new DCheckSingleton();(3)将 INSTANCE 指向堆内存空间地址 J、VM 只保证了代码执行结果的正确性、并不保证执行顺序(这里涉及到 Java 内存模型知识点在这就不多说了、感兴趣的同学可以去了解下 JVM 一些底层实现原理)所以 1,2,3 三步也可能是 1,3,2 这样我们就可能拿到的时一个半成品的对象了。
2: 涉及到类实例化知识点
3: 涉及到 Java 内存模型
4: 涉及到 JVM 的一些执行优化、指令重排等
6. 静态类部类
public class InnerSingleton {private InnerSingleton() { }
public static InnerSingleton getInstance() {return InnerClassSingleton.INSTANCE;}
private static class InnerClassSingleton{private static final InnerSingleton INSTANCE = new InnerSingleton();
}
}
这种方式和饿汉式的实现机制基本相同、都是利用了类装载机制来保证线程的安全、它和饿汉式的唯一区别就是实现了懒加载的机制、只有在调用 getInstance()方法时才去进行 InnerClassSingleton 类的实例化。
优点:避免了线程不安全,延迟加载,效率高。
7. 枚举
public enum EnumsSingleton {
INSTANCE;
@SuppressWarnings("unused")
private void method() {System.out.println("------- newInstance");
}
}
借助 JDK1.5 中添加的枚举来实现单例模式。不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象。可能是因为枚举在 JDK1.5 中才添加、所以在实际项目开发中、很少见人这么写过。
到这单例几种实现方式以及每种方式的优缺点都做了一些简单的介绍、枚举虽小但是设计的知识点很多。
优点
- 在单例模式中,活动的单例只有一个实例,对单例类的所有实例化得到的都是相同的一个实例。这样就 防止其它对象对自己的实例化,确保所有的对象都访问一个实例
- 单例模式具有一定的伸缩性,类自己来控制实例化进程,类就在改变实例化进程上有相应的伸缩性。
- 提供了对唯一实例的受控访问。
- 由于在系统内存中只存在一个对象,因此可以 节约系统资源,当 需要频繁创建和销毁的对象时单例模式无疑可以提高系统的性能。
- 允许可变数目的实例。
- 避免对共享资源的多重占用
缺点
- 不适用于变化的对象,如果同一类型的对象总是要在不同的用例场景发 生变化,单例就会引起数据的错误,不能保存彼此的状态。
- 由于单利模式中没有抽象层,因此单例类的扩展有很大的困难。
- 单例类的职责过重,在一定程度上违背了“单一职责原则”。
- 滥用单例将带来一些负面问题,如为了节省资源将数据库连接池对象设 计为的单例类,可能会导致共享连接池对象的程序过多而出现连接池溢 出;如果实例化的对象长时间不被利用,系统会认为是垃圾而被回收,这将导致对象状态的丢失。
使用场景
- 需要频繁的进行创建和销毁的对象;
- 创建对象时耗时过多或耗费资源过多,但又经常用到的对象;
- 工具类对象;
- 频繁访问数据库或文件的对象。
注意
最后在简单聊一下如何防止暴力破坏单例。主要介绍两种方式以及如何来防范这两种方式。
1: 利用 Java 的反射方式
EagerSingleton instance = EagerSingleton.getInstance();
Constructor instance2 = instance.getClass().getDeclaredConstructor();
instance2.setAccessible(true);
EagerSingleton instance3 = (EagerSingleton) instance2.newInstance();
System.out.println("===" + instance);
System.out.println("===" + instance3);
利用 Java 的反射方式可以达到爆力破解单例的效果、运行结果我就不在这贴出了有兴趣的可以自己试试 instance 和 instance3 肯定不是一个对象。
如何来防范这方式?其实也很简单 Java Security 中为我们提供了现成的方法。只需要在私有构造中使用 SecurityManager 进行检查下就可以代码如下。
// 私有的构造方法,防止外部实例化
private EagerSingleton() {SecurityManager sm = new SecurityManager();
sm.checkPermission(new ReflectPermission("禁止反射"));
}
2: 第二种方式是利用 Java 序列化和反序列化来实现
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("1.txt"));
out.writeObject(instance);
out.close();
ObjectInputStream in = new ObjectInputStream(new FileInputStream("1.txt"));
EagerSingleton readObject = (EagerSingleton) in.readObject();
in.close();
System.out.println("==" + instance);
System.out.println("==" + readObject);
如何防范?很简单只需要重写 readResolve() 反方就可以了
private Object readResolve() {return EagerSingleton.instance;}
两种暴力破解和防范的方式都介绍完了,感兴趣的同志可以去试试我这里没有贴出完整的测试代码和运行结果。
~~~~~~到这我们的小单例已经介绍完了,有没有感到惊讶!!!