单例模式学习与进阶
目录一:单例定义二:单例实现模式2.1饿汉模式2.2 懒汉模式(线程不安全)2.3懒汉模式(线程安全)2.4 双重检查模式 (DCL)2.5静态内部类单例模式2.6枚举单例2.7使用容器实现单例模式三:枚举单例的推荐3.1 一般单例的缺点3.2 序列化/反射对枚举的破坏&枚举的优点3.3 枚举单例示例四:单例模式的优缺点...
目录
一:单例定义
保证一个类仅有一个实例,并提供一个访问它的全局访问点。
单例模式结构图:
二:单例实现模式
2.1 饿汉模式
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton (){
}
public static Singleton getInstance() {
return instance;
}
}
这种方式在类加载时就完成了初始化,所以类加载较慢,但获取对象的速度快。 这种方式基于类加载机制避免了多线程的同步问题,但是也不能确定有其他的方式(或者其他的静态方法)导致类装载,这时候初始化instance显然没有达到懒加载的效果。
2.2 懒汉模式(线程不安全)
public class Singleton {
private static Singleton instance;
private Singleton (){
}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
懒汉模式申明了一个静态对象,在用户第一次调用时初始化,虽然节约了资源,但第一次加载时需要实例化,反映稍慢一些,而且在多线程不能正常工作。
2.3 懒汉模式(线程安全)
public class Singleton {
private static Singleton instance;
private Singleton (){
}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
这种写法能够在多线程中很好的工作,但是每次调用getInstance方法时都需要进行同步,造成不必要的同步开销,而且大部分时候我们是用不到同步的,所以不建议用这种模式。
2.4 双重检查模式 (DCL)
public class Singleton {
private volatile static Singleton singleton;
private Singleton (){}
public static Singleton getInstance() {
if (singleton== null) {
synchronized (Singleton.class) {
if (singleton== null) {
singleton= new Singleton();
}
}
}
return singleton;
}
}
这种写法在getSingleton方法中对singleton进行了两次判空,第一次是为了不必要的同步,第二次是在singleton等于null的情况下才创建实例。在这里用到了volatile关键字,双重检查模式是正确使用volatile关键字的场景之一。
java中的同步主要分两种:重量级的synchronized同步块,轻量级的volatile。
对象的创建可能发生指令的重排序,使用 volatile可以禁止指令的重排序,保证多线程环境内的系统安全。
在这里使用volatile会或多或少的影响性能,但考虑到程序的正确性,牺牲这点性能还是值得的。 DCL优点是资源利用率高,第一次执行getInstance时单例对象才被实例化,效率高。缺点是第一次加载时反应稍慢一些,在高并发环境下也有一定的缺陷,虽然发生的概率很小。DCL虽然在一定程度解决了资源的消耗和多余的同步,线程安全等问题,但是他还是在某些情况会出现失效的问题,也就是DCL失效,在《java并发编程实践》一书建议用静态内部类单例模式来替代DCL。
2.5 静态内部类单例模式
public class Singleton {
private Singleton(){
}
public static Singleton getInstance(){
return SingletonHolder.sInstance;
}
private static class SingletonHolder {
private static final Singleton sInstance = new Singleton();
}
}
第一次加载Singleton类时并不会初始化sInstance,只有第一次调用getInstance方法时虚拟机加载SingletonHolder 并初始化sInstance ,这样不仅能确保线程安全也能保证Singleton类的唯一性,所以推荐使用静态内部类单例模式。
2.6 枚举单例
public enum Singleton {
INSTANCE;
public void doSomeThing() {
}
}
默认枚举实例的创建是线程安全的,并且在任何情况下都是单例,上述讲的几种单例模式实现中,有一种情况下他们会重新创建对象,那就是反序列化,将一个单例实例对象写到磁盘再读回来,从而获得了一个实例。反序列化操作提供了readResolve方法,这个方法可以让开发人员控制对象的反序列化。在上述的几个方法示例中如果要杜绝单例对象被反序列化是重新生成对象,就必须加入如下方法:
private Object readResolve() throws ObjectStreamException{
return singleton;
}
枚举单例的优点就是简单,但是大部分应用开发很少用枚举,可读性并不是很高。
2.7 使用容器实现单例模式
public class SingletonManager {
private static Map<String, Object> objMap = new HashMap<String,Object>();
private Singleton() {
}
public static void registerService(String key, Objectinstance) {
if (!objMap.containsKey(key) ) {
objMap.put(key, instance) ;
}
}
public static ObjectgetService(String key) {
return objMap.get(key) ;
}
}
用SingletonManager 将多种的单例类统一管理,在使用时根据key获取对象对应类型的对象。这种方式使得我们可以管理多种类型的单例,并且在使用时可以通过统一的接口进行获取操作,降低了用户的使用成本,也对用户隐藏了具体实现,降低了耦合度。
三:枚举单例的推荐
3.1 一般单例的缺点
- 序列化及反序列化对单例的破坏
- 反射机制对单例的破坏
- 使用枚举类型实现单例模式的原因
- 使用readResolve方法解决反序列化及其弊端
该如何保证单例模式最核心的作用——“实现该模式的类有且只有一个实例对象”呢?我们知道,创建一个对象的方式有:new、克隆、序列化、反射。
- new : 由于单例模式提供的是一个私有的构造函数,所以不能外部使用new的方式创建对象。
- clone() : 虽然clone()是Object的方法,也就是说每个对象都拥有一个克隆方法,但是某一个对象直接调用clone方法,会抛出异常,即并不能成功克隆一个对象。调用该方法时,必须实现一个Cloneable 接口。这也就是原型模式的实现方式。还有即如果该类实现了cloneable接口,尽管构造函数是私有的,他也可以创建一个对象。即clone方法是不会调用构造函数的,他是直接从内存中copy内存区域的。所以克隆方式也不需要担心。
- 序列化 : 一个对象序列化成一个字节流后,若要被反序列化恢复时,会生成一个新的对象,此对象和原来的对象具有一模一样的状态,但归根结底是两个对象。
- 反射 : java中提供了反射机制,有句老话“反射可以打破一切封装!”,说明了任何类在反射机制面前都是透明的,通过反射机制可以获得类的各种属性,当然也可以获得类的构造器(就算是私有也没用),从而构造一个新的对象。(但是枚举类除外,下文会提到)。
通过上述分析,若要实现一个完美的单例模式必须考虑序列化和反射问题。
3.2 序列化/反射对枚举的破坏&枚举的优点
枚举可以防止反序列化创建新对象,防止反射创建新对象。具体原理推荐看:为什么单元素的枚举类型是单例模式的最佳实现。
3.3 枚举单例示例
单例的枚举实现在Effective Java一书中提到。因为其功能完善,使用简介,无偿地提供了序列化机制,在面对复杂的序列化或者反射攻击时任然可以绝对防止多次实例化等优点,被作者所推崇。例子如下:
public enum DataSourceEnum {
DATASOURCE;
private DBConnection connection = null;
private DataSourceEnum(){
connection = new DBConnection();
}
public DBConnection getConnection(){
return connection;
}
}
public class DBConnection {
}
public class Test {
public static void main(String[] args) {
DBConnection conn1 = DataSourceEnum.DATASOURCE.getConnection();
DBConnection conn2 = DataSourceEnum.DATASOURCE.getConnection();
System.out.println(conn1 == conn2);
}
}
四:单例模式的优缺点
4.1 优点
- 由于单例模式在内存中只有一个实例,减少了内存开支,特别是一个对象需要频繁地创建、销毁时,而且创建或销毁时性能又无法优化,单例模式减少了系统的性能开销;
- 当一个对象的产生需要比较多的资源时,如读取配置、产生其他依赖对象时,则可以通过在应用启动时直接产生一个单例对象,然后用永久驻留内存的方式来解决(在Java EE中采用单例模式时需要注意JVM垃圾回收机制)
- 单例模式可以避免对资源的多重占用,例如一个写文件动作,由于只有一个实例存在内存中,避免对同一个资源文件的同时写操作
- 单例模式可以在系统设置全局的访问点,优化和共享资源访问,例如可以设计一个单例类,负责所有数据表的映射处理
总结下就是减少系统开销,无需对频繁访问对象的创建销毁,避免资源多重占用。
4.2 缺点
- 单例模式一般没有接口,扩展很困难,若要扩展,除了修改代码基本上没有第二种途径可以实现。单例模式为什么不能增加接口呢?因为接口对单例模式是没有任何意义的,它要求“自行实例化”,并且提供单一实例、接口或抽象类是不可能被实例化的。当然,在特殊情况下,单例模式可以实现接口、被继承等,需要在系统开发中根据环境判断单例模式对测试是不利的。在并行开发环境中,如果单例模式没有完成,是不能进行测试的,没有接口也不能使用mock的方式虚拟一个对象
- 单例模式与单一职责原则有冲突。一个类应该只实现一个逻辑,而不关心它是否是单例的,是不是要单例取决于环境,单例模式把“要单例”和业务逻辑融合在一个类中
五:单例模式应用场景
在一个系统中,要求一个类有且仅有一个对象,如果出现多个对象就会出现“不良反应”,可以采用单例模式
- 需要生成唯一序列号、计数的环境
- 在整个项目中需要个共同访问资源
- 创建该对象时需要使用的资源比较多、或者大量访问了IO和数据库等资源
- 需要定义大量的静态函数及静态常量的类可以使用单例模式,当然,也可以直接定义成static
六:类加载器加载多单例
单例的目的虽然是为了确保系统中有且仅有一个对象,但是这不意味着我们不可以拿到这个单例的其他对象。如何生成第二个“单例”,关键在于类加载器:类由不同的类加载器实例加载会在方法区产生两个不同的类,在堆中生成不同Class实例。
例子很多,不赘述了 -> 例子
参考资料:
设计模式(二)单例模式的七种写法:设计模式(二)单例模式的七种写法 | BATcoder - 刘望舒
枚举实现单例 枚举实现单例_宏志有缘再见的博客-CSDN博客_枚举单例
解析——为什么单元素的枚举类型是单例模式的最佳实现 解析——为什么单元素的枚举类型是单例模式的最佳实现_whgtheone的博客-CSDN博客_枚举是单例吗
更多推荐
所有评论(0)