java设计模式—单例模式(包含单例的破坏)

    科技2024-12-27  33

    什么是单例模式?

    保证一个了类仅有一个实例,并提供一个访问它的全局访问点。

    单例模式的应用场景?

    网站的计数器,一般也是采用单例模式实现,否则难以同步;Web应用的配置对象的读取,一般也应用单例模式,这个是由于配置文件是共享的资源;数据库连接池的设计一般也是采用单例模式,因为数据库连接是一种数据库资源;多线程的线程池的设计一般也是采用单例模式,这是由于线程池要方便对池中的线程进行控制。

    单例的优缺点?

    优点:

    提供了对唯一实例的受控访问;由于在系统内存中只存在一个对象,因此可以 节约系统资源,当 需要频繁创建和销毁的对象时单例模式无疑可以提高系统的性能;避免对共享资源的多重占用

    缺点:

    不适用于变化的对象,如果同一类型的对象总是要在不同的用例场景发生变化,单例就会引起数据的错误,不能保存彼此的状态;由于单利模式中没有抽象层,因此单例类的扩展有很大的困难;单例类的职责过重,在一定程度上违背了“单一职责原则”。

    单例的创建方式

    1. 饿汉式

    类初始化时,会立即加载该对象,线程安全,效率高。

    /** * @Author 刘翊扬 * @Version 1.0 */ public class SingletonHungry { private static SingletonHungry instance = new SingletonHungry(); private SingletonHungry() {} public static SingletonHungry getInstance() { return instance; } }

    验证:

    public class Main { public static void main(String[] args) { SingletonHungry instance1 = SingletonHungry.getInstance(); SingletonHungry instance2 = SingletonHungry.getInstance(); System.out.println(instance1 == instance2); // 结果是true } }

    优点:仅实例化一次,线程是安全的。获取实例的速度快缺点:类加载的时候立即实例化对象,可能实例化的对象不会被使用,造成内存的浪费。

    2. 使用静态代码块

    ** * @author 刘翊扬 */ public class HungrySingleton2 { private static HungrySingleton2 instance = null; private HungrySingleton2() {} static { instance = new HungrySingleton2(); } private HungrySingleton2() {} public static HungrySingleton2 getInstance() { return instance; } }

    3. 懒汉式

    public class SingletonLazy { private static SingletonLazy instance; private SingletonLazy() {} public static SingletonLazy getInstance() { if (instance == null) { instance = new SingletonLazy(); } return instance; } }

    优点:在使用的时候,创建对象,节省系统资源缺点:

    如果获取实例时,初始化的工作量较多,加载速度会变慢,影响系统系能每次获取对象,都要进行非空检查,系统开销大非线程安全,当有多个线程同时调用 getInstance()方法,时,会有线程安全问题,可能导致创建多个对象

    4. 静态内部类

    /** * @Author 刘翊扬 * @Version 1.0 */ public class SingletonDemo03 { private SingletonDemo03() {} public static class SingletonClassInstance { private static final SingletonDemo03 instance = new SingletonDemo03(); } public static SingletonDemo03 getInstance() { return SingletonClassInstance.instance; } }

    优势:兼顾了懒汉模式的内存优化(使用时才初始化)以及饿汉模式的安全性(不会被反射入侵)。

    劣势:需要两个类去做到这一点,虽然不会创建静态内部类的对象,但是其 Class 对象还是会被创建,而且是属于永久带的对象。

    5. 使用枚举

    枚举本身就是单例的,一般在项目中定义常量。 例如:

    /** * @Author 刘翊扬 * @Version 1.0 */ public enum ResultCode { SUCCESS(200, "SUCCESS"), ERROR(500, "ERROR"); private Integer code; private String msg; ResultCode(Integer code, String msg) { this.code = code; this.msg = msg; } public Integer getCode() { return code; } public void setCode(Integer code) { this.code = code; } public String getMsg() { return msg; } public void setMsg(String msg) { this.msg = msg; } } ** * @Author 刘翊扬 * @Version 1.0 */ public class User { private User() {} public static User getInstance() { return SingletonDemo04.INSTANCE.getInstance(); } private static enum SingletonDemo04 { INSTANCE; // 枚举元素为单例 private User user; SingletonDemo04() { user = new User(); } public User getInstance() { return user; } } }

    解决线程安全问题

    使用双重检测锁

    public class LazySingletonDemo1 { private static LazySingletonDemo1 instance = null; public static LazySingletonDemo1 getInstance() { if (instance == null) { synchronized (LazySingletonDemo1.class) { if (instance == null) { instance = new LazySingletonDemo1(); } } } return instance; } }

    这里使用双重检测,是为了防止,当实例存在的时候,不在走同步锁,减少使用锁带来的性能的消耗。

    单例模式一定能保证只有一个实例对象吗?

    答案是:不能

    破坏单例的两种方式:

    反射反序列化

    1. 反射破坏

    通过反射是可以破坏单例的,例如使用内部类实现的单例。通过反射获取其默认的构造函数,然后使默认构造函数可访问,就可以创建新的对象了。

    /** * @Author 刘翊扬 * @Version 1.0 */ public class ReflectionDestroySingleton { public static void main(String[] args) throws IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException { SingletonLazy instance = SingletonLazy.getInstance(); Class aClass = SingletonLazy.class; // 获取默认的构造方法 Constructor<SingletonLazy> declaredConstructor = aClass.getDeclaredConstructor(); // 使默认构造方法可访问 declaredConstructor.setAccessible(true); // 创建对象 SingletonLazy instance2 = declaredConstructor.newInstance(); System.out.println(instance == instance2); // 结果是:false } }

    怎么阻止??? 可以增加一个标志位,用来判断构造函数是否被调用了。

    public class SingletonLazy { // 标志位 private static Boolean isNew = false; private static SingletonLazy instance; private SingletonLazy() { synchronized (SingletonLazy.class) { if (!isNew) { isNew = true; } else { throw new RuntimeException("单例模式被破坏!"); } } } public static SingletonLazy getInstance() { if (instance == null) { instance = new SingletonLazy(); } return instance; } }

    再次运行:

    注意: 增加标志位的确能阻止单例的破坏,但是这个代码有一个BUG,那就是如果单例是先用的反射创建的,那如果你再用正常的方法getInstance()获取单例,就会报错。因为此时标志位已经标志构造函数被调用过了。这种写法除非你能保证getInstance先于反射执行。

    public static void main(String[] args) throws IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException { Class aClass = SingletonLazy.class; // 获取默认的构造方法 Constructor<SingletonLazy> declaredConstructor = aClass.getDeclaredConstructor(); // 使默认构造方法可访问 declaredConstructor.setAccessible(true); // 创建对象 SingletonLazy instance2 = declaredConstructor.newInstance(); System.out.println("反射实例:" + instance2); // 再次调用 SingletonLazy instance = SingletonLazy.getInstance(); System.out.println(instance == instance2); // 结果是:false }

    结果:

    2. 反序列化

    SingletonLazy要实现Serializable接口

    public static void main(String[] args) throws Exception { //序列化 SingletonLazy instance1 = SingletonLazy.getInstance(); ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("tempfile.txt")); out.writeObject(SingletonLazy.getInstance()); File file = new File("tempfile.txt"); ObjectInputStream in = new ObjectInputStream(new FileInputStream(file)); //调用readObject()反序列化 SingletonLazy instance2 = (SingletonLazy) in.readObject(); System.out.println(instance1 == instance2); // 结果是:false }

    原理解释: 反序列化为什么能生成新的实例,必须从源码看起。这里分析readObject()里面的调用源码。会发现readObject()方法后进入了readObject0(false)方法。

    public final Object readObject() throws IOException, ClassNotFoundException { if (enableOverride) { return readObjectOverride(); } // if nested read, passHandle contains handle of enclosing object int outerHandle = passHandle; try { Object obj = readObject0(false); //通过debug会发现进入此方法 handles.markDependency(outerHandle, passHandle); ClassNotFoundException ex = handles.lookupException(passHandle); if (ex != null) { throw ex; } if (depth == 0) { vlist.doCallbacks(); } return obj; } finally { passHandle = outerHandle; if (closed && depth == 0) { clear(); } } }

    分析readObject0方法,通过debug进入了readOrdinaryObject()方法。

    private Object readObject0(Class<?> type, boolean unshared) throws IOException { boolean oldMode = bin.getBlockDataMode(); if (oldMode) { int remain = bin.currentBlockRemaining(); if (remain > 0) { throw new OptionalDataException(remain); } else if (defaultDataEnd) { /* * Fix for 4360508: stream is currently at the end of a field * value block written via default serialization; since there * is no terminating TC_ENDBLOCKDATA tag, simulate * end-of-custom-data behavior explicitly. */ throw new OptionalDataException(true); } bin.setBlockDataMode(false); } byte tc; while ((tc = bin.peekByte()) == TC_RESET) { bin.readByte(); handleReset(); } depth++; totalObjectRefs++; try { switch (tc) { .... 省略部分源码 case TC_OBJECT: if (type == String.class) { throw new ClassCastException("Cannot cast an object to java.lang.String"); } return checkResolve(readOrdinaryObject(unshared)); // 通过debug发现进入到了readOrdinaryObject()方法。 .... 省略部分源码 } } finally { depth--; bin.setBlockDataMode(oldMode); } }

    通过分析,readOrdinaryObject()中有两处关键代码,其中关键代码1中的关键语句为:

    此处代码是通过描述对象desc,先判断类是否可以实例化,如果可以实例化,则执行desc.newInstance()通过反射实例化类,否则返回null。

    obj = desc.isInstantiable() ? desc.newInstance() : null;

    private Object readOrdinaryObject(boolean unshared) throws IOException { if (bin.readByte() != TC_OBJECT) { throw new InternalError(); } ObjectStreamClass desc = readClassDesc(false); desc.checkDeserialize(); Class<?> cl = desc.forClass(); if (cl == String.class || cl == Class.class || cl == ObjectStreamClass.class) { throw new InvalidClassException("invalid class descriptor"); } Object obj; try { // 关键代码========= obj = desc.isInstantiable() ? desc.newInstance() : null; } catch (Exception ex) { throw (IOException) new InvalidClassException( desc.forClass().getName(), "unable to create instance").initCause(ex); } passHandle = handles.assign(unshared ? unsharedMarker : obj); ClassNotFoundException resolveEx = desc.getResolveException(); if (resolveEx != null) { handles.markException(passHandle, resolveEx); } if (desc.isExternalizable()) { readExternalData((Externalizable) obj, desc); } else { readSerialData(obj, desc); } handles.finish(passHandle); if (obj != null && handles.lookupException(passHandle) == null && desc.hasReadResolveMethod()) { Object rep = desc.invokeReadResolve(obj); if (unshared && rep.getClass().isArray()) { rep = cloneArray(rep); } if (rep != obj) { // Filter the replacement object if (rep != null) { if (rep.getClass().isArray()) { filterCheck(rep.getClass(), Array.getLength(rep)); } else { filterCheck(rep.getClass(), -1); } } handles.setObject(passHandle, obj = rep); } } return obj; }

    通过断点调试,发现其调用了desc.newInstance()方法。

    我们知道调用newInstance()方法,一定会走类的无参构造方法,但是上面通过debug我们发现,cons的类型是Object类型,所以,这里面应该是调用类Object的无参构造方法,而不是SingletonLazy类的无参构造

    那么怎么改造呢????

    继续debug调试:查看readOrdinaryObject()方法

    发现,desc.hasReadResolveMethod()这个方法返回的false,所以导致没有执行if条件下面的语句。

    desc.hasReadResolveMethod() // 从方法名可以看到,这个方法的名字是检查desc这个(SingleLazy)对象有没有readResolve()方法。

    我们现在阻止破坏单例,应该只需要在SingleLazy类中,实现自己的readResolve()方法即可。

    public Object readResolve() { return instance; }

    现在我们在看看运行的结果:为true

    大功告成。。。。

    Processed: 0.013, SQL: 8