单例模式
单例模式的破坏示例
类图:
单例模式的优点
● 由于单例模式在内存中只有一个实例,减少了内存开支,特别是一个对象需要频繁地创建、销毁时,而且创建或销毁时性能又无法优化,单例模式的优势就非常明显。
● 由于单例模式只生成一个实例,所以减少了系统的性能开销,当一个对象的产生需要比较多的资源时,如读取配置、产生其他依赖对象时,则可以通过在应用启动时直接产生一
个单例对象,然后用永久驻留内存的方式来解决(在Java EE中采用单例模式时需要注意JVM垃圾回收机制)。
● 单例模式可以避免对资源的多重占用,例如一个写文件动作,由于只有一个实例存在内存中,避免对同一个资源文件的同时写操作。
● 单例模式可以在系统设置全局的访问点,优化和共享资源访问,例如可以设计一个单例类,负责所有数据表的映射处理。
单例模式的缺点
● 单例模式一般没有接口,扩展很困难,若要扩展,除了修改代码基本上没有第二种途径可以实现。单例模式为什么不能增加接口呢?因为接口对单例模式是没有任何意义的,它
要求“自行实例化”,并且提供单一实例、接口或抽象类是不可能被实例化的。当然,在特殊情况下,单例模式可以实现接口、被继承等,需要在系统开发中根据环境判断。
● 单例模式对测试是不利的。在并行开发环境中,如果单例模式没有完成,是不能进行测试的,没有接口也不能使用mock的方式虚拟一个对象。
● 单例模式与单一职责原则有冲突。一个类应该只实现一个逻辑,而不关心它是否是单例的,是不是要单例取决于环境,单例模式把“要单例”和业务逻辑融合在一个类中。
单例模式的使用场景
在一个系统中,要求一个类有且仅有一个对象,如果出现多个对象就会出现“不良反应”,可以采用单例模式,具体的场景如下:
● 要求生成唯一序列号的环境;
● 在整个项目中需要一个共享访问点或共享数据,例如一个Web页面上的计数器,可以不用把每次刷新都记录到数据库中,使用单例模式保持计数器的值,并确保是线程安全的;
● 创建一个对象需要消耗的资源过多,如要访问IO和数据库等资源;
● 需要定义大量的静态常量和静态方法(如工具类)的环境,可以采用单例模式(当然,也可以直接声明为static的方式)。
最佳实践
单例模式是23个模式中比较简单的模式,应用也非常广泛,如在Spring中,每个Bean默认就是单例的,这样做的优点是Spring容器可以管理这些Bean的生命期,决定什么时候创建
出来,什么时候销毁,销毁的时候要如何处理,等等。如果采用非单例模式(Prototype类型),则Bean初始化后的管理交由J2EE容器,Spring容器不再跟踪管理Bean的生命周期。
1.描述
Singleton(单例)是设计模式的一种,为了保证一个类仅有一个实例,并提供一个访问它的全局访问点。
2.主要特点
1)单例类确保自己只有一个实例(构造函数私有:不被外部实例化,也不被继承)。
2)单例类必须自己创建自己的实例。
3)单例类必须为其他对象提供唯一的实例。
3.单例模式的应用
资源管理器,回收站,打印机资源,线程池,缓存,配置信息类,管理类,控制类,门面类,代理类通常被设计为单例类
如果程序有多个类加载器又同时使用单例模式就有可能多个单例并存就要找相应解决方法了
4.实现方法
如果应用程序总是创建并使用单例实例或在创建和运行时开销不大。
1).Eager initialization 饿汉式单例类(依赖jvm在加载类时创建唯一单例实例)
public class EagerSingleton { // jvm保证在任何线程访问uniqueInstance静态变量之前一定先创建了此实例 private static EagerSingleton uniqueInstance = new EagerSingleton(); // 私有的默认构造子,保证外界无法直接实例化 private EagerSingleton() { } // 提供全局访问点获取唯一的实例 public static EagerSingleton getInstance() { return uniqueInstance; } }
如果开销比较大,希望用到时才创建就要考虑延迟实例化,或者Singleton的初始化需要某些外部资源(比如网络或存储设备),就要用后面的方法了.
2)Lazy initialization 懒汉式单例类
public class LazySingleton { private static LazySingleton uniqueInstance; private LazySingleton() { } public static synchronized LazySingleton getInstance() { if (uniqueInstance == null) uniqueInstance = new LazySingleton(); return uniqueInstance; } }
同步一个方法可能造成程序执行效率下降100倍,完全没有必要每次调用getInstance都加锁,事实上我们只想保证一次初始化成功,其余的快速返回而已,如果在getInstance频繁使用的地方就要考虑重新优化了.
3)”双检锁”(Double-Checked Lock)
尽量将”加锁”推迟,只在需要时”加锁”(仅适用于java 5.0 以上版本,volatile保证原子操作)
happens-before:”什么什么一定在什么什么之前运行”,也就是保证顺序性.
现在的CPU有乱序执行的能力(也就是指令会乱序或并行运行,可以不按我们写代码的顺序执行内存的存取过程),并且多个CPU之间的缓存也不保证实时同步,只有上面的happens-before所规定的情况下才保证顺序性.
JVM能够根据CPU的特性(CPU的多级缓存系统、多核处理器等)适当的重新排序机器指令,使机器指令更符合CPU的执行特点,最大限度的发挥机器的性能.
如果没有volatile修饰符则可能出现一个线程t1的B操作和另一线程t2的C操作之间对instance的读写没有happens-before,可能会造成的现象是t1的B操作还没有完全构造成功,但t2的C已经看到instance为非空,这样t2就直接返回了未完全构造的instance的引用,t2想对instance进行操作就会出问题.
volatile 的功能:
1. 避免编译器将变量缓存在寄存器里
2. 避免编译器调整代码执行的顺序
优化器在用到这个变量时必须每次都小心地重新读取这个变量的值,而不是使用保存在寄存器里的备份。
public class DoubleCheckedLockingSingleton { // java中使用双重检查锁定机制,由于Java编译器和JIT的优化的原因系统无法保证我们期望的执行次序。 // 在java5.0修改了内存模型,使用volatile声明的变量可以强制屏蔽编译器和JIT的优化工作 private volatile static DoubleCheckedLockingSingleton uniqueInstance; private DoubleCheckedLockingSingleton() { } public static DoubleCheckedLockingSingleton getInstance() { if (uniqueInstance == null) { synchronized (DoubleCheckedLockingSingleton.class) { if (uniqueInstance == null) { uniqueInstance = new DoubleCheckedLockingSingleton(); } } } return uniqueInstance; } }
4)Lazy initialization holder class 满足所有 Double-Checked Locking 满足的条件,并且没有显示的同步操作
public class LazyInitHolderSingleton { private LazyInitHolderSingleton() { } private static class SingletonHolder { private static final LazyInitHolderSingleton INSTANCE = new LazyInitHolderSingleton(); } public static LazyInitHolderSingleton getInstance() { return SingletonHolder.INSTANCE; } }
5)内部类方式:
package com.effective.singleton; public class Elvis { private static boolean flag = false; private Elvis(){ } private static class SingletonHolder{ private static final Elvis INSTANCE = new Elvis(); } public static Elvis getInstance() { return SingletonHolder.INSTANCE; } public void doSomethingElse() { } }
但这都是基于一个条件:确保不会通过反射机制调用私有的构造器。
这里举个例子,通过JAVA的反射机制来“攻击”单例模式:
package com.effective.singleton; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; public class ElvisReflectAttack { public static void main(String[] args) throws InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException, NoSuchMethodException, SecurityException { Class<?> classType = Elvis.class; Constructor<?> c = classType.getDeclaredConstructor(null); c.setAccessible(true); Elvis e1 = (Elvis)c.newInstance(); Elvis e2 = Elvis.getInstance(); System.out.println(e1==e2); } }
运行结果:false
可以看到,通过反射获取构造函数,然后调用setAccessible(true)就可以调用私有的构造函数,所有e1和e2是两个不同的对象。
如果要抵御这种攻击,可以修改构造器,让它在被要求创建第二个实例的时候抛出异常。
经修改后:
package com.effective.singleton; public class ElvisModified { private static boolean flag = false; private ElvisModified(){ synchronized(ElvisModified.class) { if(flag == false) { flag = !flag; } else { throw new RuntimeException("单例模式被侵犯!"); } } } private static class SingletonHolder{ private static final ElvisModified INSTANCE = new ElvisModified(); } public static ElvisModified getInstance() { return SingletonHolder.INSTANCE; } public void doSomethingElse() { } }
测试代码:
package com.effective.singleton; import java.lang.reflect.Constructor; public class ElvisModifiedReflectAttack { public static void main(String[] args) { try { Class<ElvisModified> classType = ElvisModified.class; Constructor<ElvisModified> c = classType.getDeclaredConstructor(null); c.setAccessible(true); ElvisModified e1 = (ElvisModified)c.newInstance(); ElvisModified e2 = ElvisModified.getInstance(); System.out.println(e1==e2); } catch (Exception e) { e.printStackTrace(); } } }
运行结果:
Exception in thread "main" java.lang.ExceptionInInitializerError at com.effective.singleton.ElvisModified.getInstance(ElvisModified.java:27) at com.effective.singleton.ElvisModifiedReflectAttack.main(ElvisModifiedReflectAttack.java:17) Caused by: java.lang.RuntimeException: 单例模式被侵犯! at com.effective.singleton.ElvisModified.<init>(ElvisModified.java:16) at com.effective.singleton.ElvisModified.<init>(ElvisModified.java:7) at com.effective.singleton.ElvisModified$SingletonHolder.<clinit>(ElvisModified.java:22) ... 2 more
可以看到,成功的阻止了单例模式被破坏。
6)枚举方式
从JDK1.5开始,实现Singleton还有新的写法,只需编写一个包含单个元素的枚举类型。推荐写法:
package com.effective.singleton; public enum SingletonClass { INSTANCE; public void test() { System.out.println("The Test!"); } }
测试代码:
package com.effective; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import com.effective.singleton.SingletonClass; public class TestMain { public static void main(String[] args) throws NoSuchMethodException, SecurityException, InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException { Class<SingletonClass> classType = SingletonClass.class; Constructor<SingletonClass> c = (Constructor<SingletonClass>) classType.getDeclaredConstructor(); c.setAccessible(true); c.newInstance(); } }
运行结果:
Exception in thread "main" java.lang.NoSuchMethodException: com.effective.singleton.SingletonClass.<init>()
at java.lang.Class.getConstructor0(Unknown Source)
at java.lang.Class.getDeclaredConstructor(Unknown Source)
at com.effective.TestMain.main(TestMain.java:22)
由此可见这种写法也可以防止单例模式被“攻击”。
但是枚举型单例可以被克隆破坏。
7)容器式单例:
8)线程单例Threadlocal
破坏单例的典型场景有:
- 反射
- 序列化
- 克隆
- 饿汉:因为加载类的时候就创建实例,所以线程安全(多个ClassLoader存在时例外)。缺点是不能延时加载。
- 懒汉:需要加锁才能实现多线程同步,但是效率会降低。优点是延时加载。
- 双重校验锁:麻烦,在当前Java内存模型中不一定都管用,某些平台和编译器甚至是错误的,因为instance = new MaYun()这种代码在不同编译器上的行为和实现方式不可预知。
- 静态内部类:延迟加载,减少内存开销。因为用到的时候才加载,避免了静态field在单例类加载时即进入到堆内存的permanent代而永远得不到回收的缺点(大多数垃圾回收算法是这样)。
- 枚举:很好,不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象。
而且这种写法也可以防止序列化破坏单例模式,具体不在举例了,有关序列化以及单例模式被序列化破坏可以参考博文《JAVA序列化》(链接:http://blog.csdn.net/u013256816/article/details/50474678)。
单元素的枚举类型已经成为实现Singleton模式的最佳方法。