单例模式的破坏
前言:这只是我的一个课堂分享,觉得有意思,想用最简单,最轻松的方式来对单例模式的破坏进行讲解,故有此博客
此博客并不想以专业角度去解析此命题,只是想让Java苦手去欣赏Java的魅力
背景
小明是我们的主人公,也是我众多子民中的一员。他在我的电脑里生活了许久,为我做牛做马,没时间找对象。我作为慈爱的上帝于心不忍,于是令他去找一个独一无二的女朋友。
小明的思考
独一无二?单例模式!
“关于什么是独一无二,似乎并没有一个定义?”,小明想到,“或许就是整个程序世界只能拥有一个的对象。”
于是便引出了单例模式~
public class Friend { static private Friend friend; // 这是一个构造函数,但是构造函数使用private修饰的 // private 是私有的意思,被这个修饰过的方法或属性是无法在外部访问到的 private Friend() { System.out.println("小明找到了一个对象"); } // 懒汉模式(与饿汉模式相对,不在此赘述) // 想要得到Friend对象只能通过此方法获得 static public Friend findFriend() { if (friend == null) { friend = new Friend(); } System.out.println("小明介绍了他的对象"); return friend; } }
这个类就是一个单例模式,由于构造函数为私有的无法再外部的类进行调用,即在外部类无法通过以下方法获得一个新的对象。
Friend friend = new Friend();
这时聪明的二壮想到:
“既然我没办法创造对象,那我应该怎么使用这个对象呢?”
别急,注意看下面方法
static public Friend findFriend() { if (friend == null) { friend = new Friend(); } System.out.println("小明介绍了他的对象"); return friend; }
这个方法其实非常简单,当调用这个方法的时候进行判断,当friend属性为null的时候(此时friend并没有赋值),我们就为friend赋值并返回。如果赋值过了,则直接返回……
这时二壮又打断我:
”不对啊,我英俊潇洒又慈爱的上帝,你不是刚刚说不能用
Friend friend = new Friend();
方法创建吗?!“
我咬牙切齿并白了他一眼温和的解释道:
private只是对外部的类进行修饰,并不会影响这个类的使用,即在Friend类中是可以使用此方法的!
综上所述,如果我们想要在其他类中找到小明的对象,我们只能通过
Friend.findFriend();
的形式创建对象,如
public class run { public static void main(String[] args) { Friend friend; // 此时无法直接创造一个对象 // friend = new Friend(); friend = Friend.findFriend(); } }
那么,这样一个单例模式,也就是独一无二的对象就被小明找到啦~
对小明对象的攻防战!
很久很久之后, 由于小明非常好的完成了我的任务,还有了一个独一无二的对象,有点得意忘形。每次看到其他人就会先显摆一下。非常的影响他爆金币工作。于是,我打算给他点颜色看看,再创造一个他的对象,此时这个对象在这个程序中就会存在两个!也就是破坏单例模式
反射!对灵魂的争夺!
话是这么说,但是小明把他对象的构造方法给私有化了,即使我作为上帝也没办法从外部通过new 来创造一个对象。但换句话说,只要能得到她的构造方法,我就能通过这个构造方法来new一个对象了。因此我想到了反射!
什么是反射?
反射是一种可以通过对象来获取类的方法。
这么说可能有点抽象。但是我们都知道人与灵魂的关系。
一般情况下我们都是通过类去创建对象,如:
Friend = new Friend();
这就相当于通过灵魂去创造躯体。
而反射则是逆天改命,反其道而行之,通过躯体来提取灵魂:
// 获取到friend的类 Class<?> clazz = Friend.class;
既然都得到了灵魂,那我们就可以对这个灵魂上下其手(坏笑)。比如通过灵魂来得到构造方法,进而创造对象。
对女朋友的争夺!
那么我们就可以通过这种方式创建小明的对象
import java.lang.reflect.Constructor; public class run { public static void main(String[] args) throws Exception { Friend friend = Friend.findFriend(); Class<?> clazz = Friend.class; Constructor<?> ctor = clazz.getDeclaredConstructor(); ctor.setAccessible(true); Friend myFriend = (Friend) ctor.newInstance(); if (!friend.equals(myFriend)) { System.out.println("破坏单例模式,抢走了小明的对象"); } } }
其中main方法的具体功能:
// 得到构造方法 Constructor<?> ctor = clazz.getDeclaredConstructor(); // 将构造方法设置为外界可访问 ctor.setAccessible(true); // 让构造方法去在创造一个对象 Friend myFriend = (Friend) ctor.newInstance();
事已至此,我们就创造了一个对象,让小明看看:
// 如果小明的对象和我创造的对象用的地址不一样,也就是她们两个不是同一个对象,则输出下面这句话 if (!friend.equals(myFriend)) { System.out.println("破坏单例模式,抢走了小明的对象"); } // 结果为 破坏单例模式,抢走了小明的对象
我们抢走了小明的对象,虽然小明非常的气馁,但是小明似乎并没有放弃……
对象就由我来守护
小明想到:事实上反射的本质也是通过构造方法去进行实例化,那我只需要在构造方法上加点小手段就可以进行保护了!于是小明便对他的对象进行大刀阔斧的改造:
public class Friend { static private Friend friend; private Friend() throws Exception { // 在构造方法中添加了一条判断,如果在已经实例化对象后还调用构造方法就会抛出异常 if (friend == null) { System.out.println("小明找到了一个对象"); } else { throw new Exception("不许再创造我的对象"); } } // 懒汉模式 static public Friend findFriend() throws Exception { if (friend == null) { friend = new Friend(); } System.out.println("小明介绍了他的对象"); return friend; } }
实际上小明并没有进行太多的改造,只是在构造方法中添加了这样的一段话:
if (friend == null) { System.out.println("小明找到了一个对象"); } else { throw new Exception("不许再创造我的对象"); }
这就跟反射的原理有关了。当我们获取到friend对象的时候,此时Friend类中的friend属性一定不是null(因为如果为null的时候会先赋值再返回)。于是反射通过对象获取类的时候,也就带上了一个赋过值的属性。此时调用构造方法时就会进行判断,发现friend属性并不为null,最后抛出错误,打断执行。
真是一个漂亮的反击。我无奈苦笑,但是你以为这就完了吗?于是我开始了新一轮的进攻
序列化!对躯体的复制!
看来直接通过构造方法去获得对象的这条路是走不通了。于是我便灵机一动想到了一个东西:照相机。如果我有一个神奇的照相机,可以通过拍照的方式,将照片变成物体,不就能在创造一个对象了吗。那么Java中有没有类似的功能呢?那肯定是有的,也就是序列化
什么是序列化
正如上文提到的照相机一般,序列化就是一个神奇的照相机,它可以将对象写入到流,此时我们在从流中读取数据,并构造成对象就可以获取到一个新的对象了!
事实上,序列化是一个挺常见的功能。我们有时候挺难避免它的。
但是要是想要使用序列化的话,得先趁小明不注意,在她对象中继承一个Serializable接口:
import java.io.Serializable; public class Friend implements Serializable { static private Friend friend; private Friend() { System.out.println("小明找到了一个对象"); } // 懒汉模式 static public Friend findFriend() { if (friend == null) { friend = new Friend(); } System.out.println("小明介绍了他的对象"); return friend; } }
这个接口只是去告诉Java,这个对象是可以序列化的,所以并不强制我们去实现任何方法。
对女朋友的复制!
于是我们就可以通过以下方式去得到对象了:
import java.io.*; public class run { public static void main(String[] args) throws IOException, ClassNotFoundException { // 得到Object输出流 ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("tempFile")); // 向输出流中写入小明的对象 oos.writeObject(Friend.findFriend()); // 得到输入流 ObjectInputStream ois = new ObjectInputStream(new FileInputStream("tempFile")); // 将输入流里的数据转换成对象 Friend myFriend = (Friend) ois.readObject(); } }
最后我们让小明看看:
if (!myFriend.equals(Friend.findFriend())) { System.out.println("抢走了对象"); } // 输出结果 抢走了对象
哈!这样我们就再一次打击到了小明。但是小明却低头思考,不一会似乎就找到了解决方案……
雕虫小技,看我拿下!
这个方法看似非常复杂,其实非常简单。如果我们跟小明一样看了看原码就会发现他的本质实际上还是反射。
原码:
private Object readOrdinaryObject(boolean unshared) throws IOException { ... 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); } ... 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; }
想必大家都看懂了吧都不想看吧。没关系,我们我们有简单版:
实例化object -> 判断是否有readResolve()方法 —如果没有—> 直接return object(破坏单例模式) —如果有—> 调用此方法并返回(单例模式没有被破坏)
那么就很简单了:如果我们没有readResolve()方法,单例模式就会被破坏,那么我们只用重写一个readResolve()方法不就行了吗!因此,对象被改造成了:
import java.io.Serializable; public class Friend implements Serializable { static private Friend friend; private Friend() { System.out.println("小明找到了一个对象"); } // 懒汉模式 static public Friend findFriend() { if (friend == null) { friend = new Friend(); } System.out.println("小明介绍了他的对象"); return friend; } // 重写了方法 private Object readResolve() { return friend; } }
那就没办法了。看来我只能用下下策了。
多线程!人海战术!
我同时叫来无数吴彦组去舔小明的对象。此时小明就没办法去保证,小明的对象一定还会跟小明走了。
我仔细研究过小明的对象,发现单纯的他并没有为他的对象为多线程做适配。也就是说,在多线程下很可能会出问题。那么问题来了,为什么会出问题呢?又会出现什么问题?
这里不在写测试的方法,因为即使写了测试,也不一定会出现期待的结果。
为什么会出问题
简单来说,由于JVM的乱序性,可能会出现各种各样的错。我叫来小强来说明这个问题:
(在这个例子中,小明和小强各表示一个进程)
时间 | 小明 | 小强 |
---|---|---|
1 | 判断friend是否为null | |
2 | 进行实例化 | |
3 | 中断 | 判断friend是否为null |
4 | 进行实例化 | |
5 | 为friend赋值 | |
6 | 获取 | |
7 | 为friend赋值 | |
8 | 获取 |
于是我们可以看到,小明与小强各赋了一次值。也就是此时小明和小强获得到的对象就不是同一个对象了。
了解过数据库事务的处理的同学可能会很好理解
除此之外,由于JVM的赋值操作分为划分空间与分配指针,所以并不确定究竟是先分配的空间还是先分配的指针
于是也可能出现这种错误:
时间 | 小明 | 小强 |
---|---|---|
1 | 判断friend是否为null | |
2 | 进行实例化 | |
3 | 分配了内存(此时friend != null) | |
4 | 判断friend是否为null(不是null) | 中断 |
5 | 获得实例 | |
6 | ERR(空指针异常) | |
7 | 赋值 | |
8 | 获得实例 |
在这个例子中,由于小强先告诉Java,friend已经有数据啦,但是却没有给friend赋值。与此同时,小明从Java中得到消息,friend有数据,于是直接获得对象。但是仔细一看,发现自己拿到的是一个null,才知道消息是假哒,这时候就会报错。
十分危险,必须处理
小明莞尔一笑,其实解决方法非常简单,事实上,我们只需要添加一个synchronized关键字就好:
public class Friend { static private Friend friend; private Friend() { System.out.println("小明找到了一个对象"); } // 添加了关键字 static synchronized public Friend findFriend() { if (friend == null) { friend = new Friend(); } System.out.println("小明介绍了他的对象"); return friend; } }
synchronized可以保证这个代码块同时只会被一个进程使用。当小明访问的时候,就算有再多吴彦组都没办法,必须得排队。当小明结束访问的时候,其他进程才能继续使用。
了解过操作系统中的“进程互斥”的同学可能会很好理解。
小明动动手指就解决了这个问题,但是几天后就又开始头疼……
一些遗留问题
原来,小明在介绍他对象的时候,总是经常排队。即使小明知道它的对象确实被创建了,也得老老实实排队,确实非常难过。那该怎么解决呢?
小明冥思苦想,寝食难安,我看他实在可怜,便稍微点拨:
public class Friend { static private Friend friend; private Friend() { System.out.println("小明找到了一个对象"); } // 这里并没有被修饰 static public Friend findFriend() { if (friend == null) { // 在这里进行限制 synchronized(friend) { if (friend == null) { friend = new Friend(); } } } System.out.println("小明介绍了他的对象"); return friend; } }
这样,当调用这个方法的时候并不会排队,只有判断确实没有对象才会排队的创建对象。如果存在对象便能直接获取。
这样子就可以在第一次调用的时候进行排队,之后获取便可以得到相同对象。
结局
小明对象的攻防战就如此结束了。终于,小明又可以老老实实给我工作了。可喜可贺可喜可贺。
这个博客只是自己查资料的个人理解,再加上这是我第一次写博客。正如开头所说,我不想要用严谨的态度来说明这个问题。但是还是支持讨论与进步,如果有纰漏可以直接告诉我哦