【单例模式与破坏】小明的恋爱攻防战

单例模式的破坏

前言:这只是我的一个课堂分享,觉得有意思,想用最简单,最轻松的方式来对单例模式的破坏进行讲解,故有此博客

此博客并不想以专业角度去解析此命题,只是想让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获得实例
6ERR(空指针异常)
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;
    }
}

这样,当调用这个方法的时候并不会排队,只有判断确实没有对象才会排队的创建对象。如果存在对象便能直接获取。

这样子就可以在第一次调用的时候进行排队,之后获取便可以得到相同对象。

结局

小明对象的攻防战就如此结束了。终于,小明又可以老老实实给我工作了。可喜可贺可喜可贺。

这个博客只是自己查资料的个人理解,再加上这是我第一次写博客。正如开头所说,我不想要用严谨的态度来说明这个问题。但是还是支持讨论与进步,如果有纰漏可以直接告诉我哦

  • 4
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

飛_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值